Search values in a two dimensional array with multiple criteria - arrays

Suppose I have the following table with three columns. I want to search for an exact match or next previous date from Column3, conditional to Column1 being a given value.
This can be easily done with XLOOKUP. However, I need to do so in VBA because I'll show the date found in a userform Textbox to the user. From what I have searched so far, Application.Worksheetfunction.Xlookup won't work with an & for multiple criteria, so the solution for this would involve manipulating arrays.
I created a variant from that table by writing:
Dim TBL As ListObject
Set TBL = Sheets("sheet1").ListObjects("Table1")
Dim DirArray As Variant
DirArray = TBL.DataBodyRange
Any advice on how to get that approximate match using arrays?

Using an array of values will be faster than referencing a cell for each check - esp. if your table is much larger.
You can use this function - it will return 0 in case no valid date is found.
As I am using sortBy you will need Excel 365 for this to work.
By using SortBy it is safe to exit the for-loop in case we have found a matching date.
Public Function nearestDate(lo As ListObject, valueColumn1 As String, valueColumn3 As Date) As Date
Dim arrValues As Variant
arrValues = Application.WorksheetFunction.SortBy(lo.DataBodyRange, lo.ListColumns(1).DataBodyRange, 1, lo.ListColumns(3).DataBodyRange, 1)
Dim i As Long
For i = 1 To UBound(arrValues, 1)
If arrValues(i, 1) = valueColumn1 Then
If arrValues(i, 3) = valueColumn3 Then
'we found what we are looking for
nearestDate = arrValues(i, 3)
ElseIf arrValues(i, 3) < valueColumn3 Then
'we have to check next row - if there is one
If i < UBound(arrValues, 1) Then
If arrValues(i + 1, 1) = valueColumn1 And arrValues(i + 1, 3) > valueColumn3 Then
'same column1 but column3 greater than valueColumn3
nearestDate = arrValues(i, 3)
ElseIf arrValues(i + 1, 1) <> valueColumn1 Then
'new column1 value --> therefore we take current date
nearestDate = arrValues(i, 3)
End If
Else
'last value --> ok
nearestDate = arrValues(i, 3)
End If
End If
End If
If nearestDate > 0 Then Exit For
Next
End Function
You can call this function like this:
Public Sub test()
Dim ws As Worksheet: Set ws = Thisworkbook.Worksheets("sheet1")
Dim lo As ListObject: Set lo = ws.ListObjects("Table1")
Dim valueColumn1 As String: valueColumn1 = ws.Range("F1")
Dim valueColumn3 As Date: valueColumn3 = ws.Range("F2")
Debug.Print nearestDate(lo, valueColumn1, valueColumn3)
End Sub

There may well be a neater answer, but here is a simple brute-force function that just scans down every row in the given data looking for the closest match to the given criteria. The function returns the date of the closest match, but maybe it would be more useful to you if it returned, say, the row number of the row that is the closest match. Put this function in a new code module so that it can be called as a function from a cell, for example =findEntryByCol1andCol3(Table1,F1,F2)
Option Explicit
Public Function findEntryByCol1andCol3(dataToSearch As Range, findCol1, findCol3) As Variant
'// variable to hold the row with the closest match to criteria
Dim matchRow As Range
Set matchRow = Nothing
'// variable to hold the row being checked
Dim checkRow As Range
Dim ix As Long
For ix = 1 To dataToSearch.Rows.Count
'// get the next row to be checked
Set checkRow = dataToSearch.Rows(ix)
'// does column 1 in this row match the search criterion for column 1?
If checkRow.Cells(1, 1).Value = findCol1 Then
'// now see if the date in the row is less than the search date
If findCol3 >= checkRow.Cells(1, 3).Value Then
'// If there has been no match then use this checked row as the first found match
If matchRow Is Nothing Then
Set matchRow = checkRow
'// If there has been a previous match check
'// if the new date is later that the previously found date
ElseIf matchRow.Cells(1, 3).Value < checkRow.Cells(1, 3).Value Then
Set matchRow = checkRow
End If
End If
Else
End If
Next ix
'// Now return the result of the search
If matchRow Is Nothing Then
findEntryByCol1andCol3 = "Not found"
Else
findEntryByCol1andCol3 = matchRow.Cells(1, 3)
End If
End Function

Related

How do I get data in cells as an array up one row in the same column without selecting?

I am trying to write a process that compares strings and deletes the duplicate string within a given column using a selection as the top and bottom constraints.
Most of the process of checking and deleting works however I am having trouble with moving the cell contents up a cell after the duplicate string was deleted.
Image showing how the script should work
Red outline is the loop that selects the String to compare against.
Green outline is the loop that finds, deletes and moves the cells up one.
Blue outline is the Selection.
Stage 1 is to find and compare two strings that are the same.
Stage 2 is to delete the string that is the same as the first string.
Stage 3 is to move everything under the deleted cell with the deleted string up one row so that there is no empty cell.
I'm having problems with stage 3. I don't know how to move all data in those cells up one row without using a loop and I can't use the selection.
Here is the code so far:
Private Sub Tabeller()
Dim vRngMv As Variant
Dim iRowChsr1, iRowChsr2, iRowTtl, iI As Integer
Dim vRowIn, vRowComp As String
Dim oRngSlct, oRngMv As Range: Dim ws As Worksheet: Dim oBS As Object
'Newer Version will get rid of Selection as range determination
'Why does oRngSlct become a Variant/Object/Range here and oRngMv stays a Range object?
'I dont use it, kept it in to ask the question.
Set oRngMv = Selection: Set oRngSlct = Selection
iRowTtl = oRngSlct.Rows.Count
'First Loop For holding target cell data for comparison
For iRowChsr1 = 1 To iRowTtl
'Chooses target cell and string
vRowIn = oRngSlct(iRowChsr1, 1)
'Second loop for Seeking a matching String
For iRowChsr2 = 1 To iRowTtl
'Check to not pick itself
If iRowChsr1 = iRowChsr2 Then
'Offsets Counter by 1 if it enocunters itself
iRowChsr2 = iRowChsr2 + 1
Else
'Sets comparison string
vRowComp = oRngSlct(iRowChsr2, 1)
'String comparison
iI = StrComp(vRowIn, vRowComp, 1)
'If strings are equal
If iI = 0 Then
'Deletes; I know this is redundant but its here for clarity
oRngSlct(iRowChsr2, 1) = ""
'Offsets by iRowChsr by 1
iRowChsr2 = iRowChsr2 + 1
'Create Variant with proper range, it just has to be translated into something that excel can move.
vRngMv = Range((oRngSlct(iRowChsr2, 1)), (oRngSlct(iRowTtl, 1)))
Set oRngMv = Range 'I know this doesnt work
'Offsets back to original Position of Deleted cell
iRowChsr2 = iRowChsr2 - 1
'*******************************
'*Cuts and pastes or moves here*
'*******************************
End If
End If
'Next Comparison String
Next iRowChsr2
'Next target String
Next iRowChsr1
End Sub
Unique (Remove Duplicates)
You could rather use one of the following.
The first solution will leave error values and blanks as part of the resulting data, while the second one will remove them.
The Code
Option Explicit
Sub removeDupesColumnSelection()
' Validate Selection.
If TypeName(Selection) <> "Range" Then Exit Sub
' Remove duplicates.
Selection.Columns(1).RemoveDuplicates Array(1), xlNo
End Sub
Sub uniquifyColumnSelection()
' Validate Selection.
If TypeName(Selection) <> "Range" Then Exit Sub
' Write values from first column of Selection to Data Array.
Dim rg As Range: Set rg = Selection.Columns(1)
Dim rCount As Long: rCount = rg.Rows.Count
Dim Data As Variant
If rCount > 1 Then
Data = rg.Value
Else
ReDim Data(1 To 1, 1 To 1): Data(1, 1) = rg.Value
End If
' In Unique Dictionary...
With CreateObject("Scripting.Dictionary")
.CompareMode = vbTextCompare
' Write unique values from Data Array to Unique Dictionary.
Dim Key As Variant
Dim i As Long
For i = 1 To rCount
Key = Data(i, 1)
If Not IsError(Key) Then
If Len(Key) > 0 Then
.Item(Key) = Empty
End If
End If
Next i
ReDim Data(1 To rCount, 1 To 1)
If .Count > 1 Then
' Write values from Unique Dictionary to Data Array.
i = 0
For Each Key In .Keys
i = i + 1
Data(i, 1) = Key
Next Key
End If
End With
' Write values from Data Array to Destination Range.
rg.Value = Data
End Sub

Excel VBA Paste Non-Empty Array Items into Cell Range Containing Formulas

I have two columns of data in a spreadsheet.
Column A has either cells containing "X" or empty cells and Column B contains formulas.
I want to use VBA to pull Column A into an array, and paste the array into Column B, making sure the "X"s copy over but the empty array elements do not.
The method I have looks at each array element and if it is an "X" then paste that 1 element, it works but its slow for large data pools. Is there a faster method?
See code below:
Option Explicit
Sub Test()
Dim array1 As Variant, i As Integer
array1 = Sheets("Sheet1").Range("A2:A8").Value
For i = 1 To UBound(array1)
If array1(i, 1) = "X" Then
Sheets("Sheet1").Cells(i + 1, 2) = array1(i, 1)
End If
Next i
End Sub
use a second array to hold the formula in B. Then iterate both arrays and replace the second with the value where needed:
Sub Test()
With Sheets("Sheet1")
Dim aArr() As Variant
aArr = .Range("A2:A8").Value
Dim bArr() As Variant
bArr = .Range("B2:B8").Formula
Dim i As Long
For i = 1 To UBound(aArr, 1)
If aArr(i, 1) = "X" Then
bArr(i, 1) = aArr(i, 1)
End If
Next i
.Range("B2:B8").Formula = bArr
End With
End Sub
Replace Formulas with Criteria
It is assumed that
the worksheet is in ThisWorkbook, the workbook containing this code,
the Data Column is adjacent to the right of the Criteria Column, which is defined by FirstCellAddress,
the 'search' for the Criteria (X) is case-sensitive i.e. X <> x.
The Code
Option Explicit
Sub replaceFormulasWithCriteria()
Const wsName As String = "Sheet1"
Const FirstCellAddress As String = "A2"
Const Criteria As String = "X"
' Define Criteria Column Range.
Dim rng As Range
With ThisWorkbook.Worksheets(wsName).Range(FirstCellAddress)
Set rng = .Resize(.Worksheet.Cells(.Worksheet.Rows.Count, .Column) _
.End(xlUp).Row - .Row + 1)
End With
' Write values from Criteria Column Range to Criteria Array.
Dim Crit As Variant: Crit = rng.Value
' Define Data Column Range.
Set rng = rng.Offset(, 1)
' Write formulas from Data Column Range to Data Array.
Dim Data As Variant: Data = rng.Formula
Dim i As Long
' Loop through rows of Criteria/Data Column Range.
For i = 1 To UBound(Data, 1)
' Check if Criteria is found in current row in Criteria Array.
If Crit(i, 1) = Criteria Then
' Write Criteria to current row in Data Array.
Data(i, 1) = Criteria
End If
Next i
' Write modified values from Data Array to Data Column Range.
rng.Value = Data
' or:
'rng.Formula = Data
End Sub

How do I build an array based on data using two criteria (Array Formula [pref] or VBA)

I have the following example data:
The first column is a list of names, the second column is the year those names belong to.
What I want to do is build a list of all unique (distinct) names from one year.
So for example in the year 2016 I want it to build a list like in the end result column, while in the year 2017 I want it to build a list with the unique names of that year.
Preferably I want it to be a (dynamic) named range so that the calculation only has to be performed once and so that I can use the =INDEX(examplenamedrange, 1) formula to call the names that I want to use.
If this is not possible in a dynamic named range then storing the array in VBA is also a possibility.
I’ve seen a few Excel formulas around the net that look at unique values in a list, but none that I could find with an extra criteria (in this case: Year).
Can anyone set me on the right path?
Here is a short VBA sub to achieve what you ask.
To set up a sub, press Alt+F11 to open VBA editor, then Insert>Module and paste the following code. I have commented it to show what each section is doing. You could also set this up to run when the Year cell is changed, but I'll leave that as an exercise for you! To run it, press F5 in the VBA editor or click the run button.
Sub uniqueInYear()
Dim sh As Worksheet
Set sh = ActiveSheet
Dim vcell As Range
Dim namesString As String
namesString = ""
Dim namesList() As String
' Compile string with all names comma separated for given year
For Each vcell In Range("A2:A" & sh.UsedRange.Rows.Count)
' check if name already captured for given year
If InStr(namesString, vcell.Value) = 0 And vcell.Offset(0, 1).Value = sh.Range("E1").Value Then
namesString = namesString & "," & vcell.Value
End If
Next vcell
' If empty then quit
If namesString = "" Then
Exit Sub
End If
' Remove leading comma
namesString = Right(namesString, Len(namesString) - 1)
' Put names into array
namesList = Split(namesString, ",")
' Write names to result column after clearing it
sh.Range("E2:E" & sh.UsedRange.Rows.Count + 1).Value = ""
Dim nameVar As Variant
For Each nameVar In namesList
sh.Range("E" & sh.UsedRange.Rows.Count + 1).End(xlUp).Offset(1, 0).Value = nameVar
Next nameVar
' Named range - delete if it exists then create a-fresh
On Error Resume Next
sh.Parent.Names("UniqueNames").Delete
On Error GoTo 0
sh.Parent.Names.Add name:="UniqueNames", _
RefersTo:=sh.Range("E2", sh.Range("E" & sh.UsedRange.Rows.Count + 1).End(xlUp))
End Sub
Outcome:
Just to add another similar, but different method in there. You can use a UDF which returns an array. So paste the code into a code module and then use the following formula on the worksheet
=GetNamesInYear(names,dates,2016)
where names is your range of names, dates is your range of dates and 2016 is the year you are searching, either a number written in the formula or reference to a cell with the value 2016 is fine.
To return the full array you need to enter the formula with Ctrl+Shift+Enter. To view all results, rather than just the first result, highlight that cell and the 5 below it (say), press F2 to edit and then Ctrl+Shift+Enter again.
Alternatively you can access the array with any worksheet function that can deal with string arrays. For example:
=INDEX(GetNamesInYear(names,dates,2016),2)
returns the second item in the array
Here is the code
Function GetNamesInYear(names As Range, years As Range, year As Integer) As Variant
Dim namesArr As Variant
namesArr = names.Value2
Dim yearsArr As Variant
yearsArr = years.Value2
Dim results As Long
results = 0
Dim resultArr As Variant
Dim i As Long
ReDim resultArr(0 To 0)
For i = 1 To UBound(namesArr, 1)
If Not InArray(resultArr, namesArr(i, 1)) And (yearsArr(i, 1) = year) Then
ReDim Preserve resultArr(0 To results)
resultArr(results) = namesArr(i, 1)
results = results + 1
End If
Next i
GetNamesInYear = Application.WorksheetFunction.Transpose(resultArr)
End Function
Private Function InArray(arr As Variant, value As Variant) As Boolean
Dim i As Integer
For i = 0 To UBound(arr)
If arr(i) = value Then
InArray = True
Exit Function
End If
Next i
InArray = False
End Function
The result looks like this:
Update
Names and dates inputs are now split out (separate ranges) as per comments from OP
You could try this:
Sub Names()
Dim x, Years, Counted, ColumnCount, j, lColumn
Dim Names(), FoundNames()
Years = Range("B1").Value
Counted = 0
ColumnCount = 2
ReDim Names(Range("A" & Rows.count).End(xlUp).row)
ReDim FoundNames(LBound(Names) To UBound(Names))
lColumn = Cells(1, Cells(1, Columns.count).End(xlToLeft).Column).Column
For Each c In Range(Range("A1"), Range("A" & Rows.count).End(xlUp))
If Years <> Range("B" & c.row).Value Then
For i = LBound(Names) To UBound(Names)
If Names(i) <> "" Then
j = j + 1
FoundNames(j - 1) = Names(i)
End If
Next i
ReDim Preserve FoundNames(LBound(Names) To j - 1)
Cells(1, lColumn + ColumnCount).Value = Years
For i = LBound(FoundNames) To UBound(FoundNames)
Cells(i + 2, lColumn + ColumnCount).Value = FoundNames(i)
Next
ColumnCount = ColumnCount + 1
Years = Range("B" & c.row).Value
Counted = 0
ReDim Names(Range("A" & Rows.count).End(xlUp).row)
ReDim FoundNames(LBound(Names) To UBound(Names))
End If
If InStr(Join(Names, ","), c.Value) < 1 Then
Names(Counted) = c.Value
Counted = Counted + 1
End If
Next c
j = 0
For i = LBound(Names) To UBound(Names)
If Names(i) <> "" Then
j = j + 1
FoundNames(j - 1) = Names(i)
End If
Next i
ReDim Preserve FoundNames(LBound(Names) To j - 1)
Cells(1, lColumn + ColumnCount).Value = Years
For i = LBound(FoundNames) To UBound(FoundNames)
Cells(i + 2, lColumn + ColumnCount).Value = FoundNames(i)
Next
End Sub
The results look like this:
Array formula can work here:
=INDEX($A$1:$A$15, N(IF({1}, MODE.MULT(IF(($B$1:$B$15=2016)*(ROW($A$1:$A$15)=MATCH($A$1:$A$15, $A$1:$A$15, 0)), (ROW($A$1:$A$15)) * {1,1})))))
Define your named range as dynaRange_2016, and see it's use in the two images
:
You could name a range for each year instead, and then define yet another name for the uniques range. This is more versatile:
define Named range_2017 as =INDEX(Sheet5!$A:$A, MATCH(2017,Sheet5!$B:$B, 0)):INDEX(Sheet5!$A:$A, MATCH(2017,Sheet5!$B:$B, 1))
Then define another named range uniques_2017 as=INDEX(Sheet5!range_2017, N(IF({1}, MODE.MULT(IF(ROW(Sheet5!range_2017)-MATCH(2017, Sheet5!$B:$B, 0)+1=MATCH(Sheet5!range_2017, Sheet5!range_2017, 0), (ROW(Sheet5!range_2017)-MATCH(2017, Sheet5!$B:$B, 0)+1) * {1,1})))))
In your sheet, you can call as INDEX(uniques_2017, 3) for example. Do the same for all years that you expect to occur.

Sum up column B based on colum C values

I have a quick question: I try to sum up in a table of 4 columns column number 2 if the value in column number 1 AND 3 matches. I found a sample code here on stack overflow, but it counts currently based on column 1. I'm new to VBA and don't know what to change or how to adjust the code to base my calculations on column 1 and 3. Here is the sample code:
Option Explicit
Sub testFunction()
Dim rng As Excel.Range
Dim arrProducts() As String
Dim i As Long
Set rng = Sheet1.Range("A2:A9")
arrProducts = getSumOfCountArray(rng)
Sheet2.Range("A1:B1").Value = Array("Product", "Sum of Count")
' go through array and output to Sheet2
For i = 0 To UBound(arrProducts, 2)
Sheet2.Cells(i + 2, "A").Value = arrProducts(0, i)
Sheet2.Cells(i + 2, "B").Value = arrProducts(1, i)
Next
End Sub
' Pass in the range of the products
Function getSumOfCountArray(ByRef rngProduct As Excel.Range) As String()
Dim arrProducts() As String
Dim i As Long, j As Long
Dim index As Long
ReDim arrProducts(1, 0)
For j = 1 To rngProduct.Rows.Count
index = getProductIndex(arrProducts, rngProduct.Cells(j, 1).Value)
If (index = -1) Then
' create value in array
ReDim Preserve arrProducts(1, i)
arrProducts(0, i) = rngProduct.Cells(j, 1).Value ' product name
arrProducts(1, i) = rngProduct.Cells(j, 2).Value ' count value
i = i + 1
Else
' value found, add to id
arrProducts(1, index) = arrProducts(1, index) + rngProduct.Cells(j, 2).Value
End If
Next
getSumOfCountArray = arrProducts
End Function
Function getProductIndex(ByRef arrProducts() As String, ByRef strSearch As String) As Long
' returns the index of the array if found
Dim i As Long
For i = 0 To UBound(arrProducts, 2)
If (arrProducts(0, i) = strSearch) Then
getProductIndex = i
Exit Function
End If
Next
' not found
getProductIndex = -1
End Function
Sum Column B based on Column A using Excel VBA Macro
Could you please advise me how I can solve this problem. Below you can find a sample picture of my small table. The quantity of the yellow part, for instance, shall be summed up and the second row shall be deleted.
Sample Table - Picture
you said "I try to sum up in a table of 4 columns column number 2" but from your "Sample Table - Picture" I'd understand you want to sum up column number 4
edited after OP variation of data range
Assuming what above you could try the following
Option Explicit
Sub main()
On Error GoTo 0
With ActiveSheet '<== set here the actual sheet reference needed
' With .Range("A:D").Resize(.cells(.Rows.Count, 1).End(xlUp).row) '<== here adjust "A:D" to whatever colums range you need
With .Range("A51:D" & .cells(.Rows.Count, "A").End(xlUp).row) '<== here adjust "A:D" to whatever colums range you need
With .Offset(1).Resize(.Rows.Count - 1)
.Offset(, .Columns.Count).Resize(, 1).FormulaR1C1 = "=SUMIFS(C2, C1,RC1,C3, RC3)" '1st "helper column is the 1st column at the right of data columns (since ".Offset(, .Columns.Count)")
.Columns(2).Value = .Offset(, .Columns.Count).Resize(, 1).Value 'reference to 1st "helper" column (since ".Offset(, .Columns.Count)")
.Offset(, .Columns.Count).Resize(, 1).FormulaR1C1 = "=concatenate(RC1,RC3)"
With .Offset(, .Columns.Count + 1).Resize(, 1) '2nd "helper" column is the 2nd column at the right of data columns (since ".Offset(, .Columns.Count + 1)"
.FormulaR1C1 = "=IF(countIF(R1C[-1]:RC[-1],RC[-1])=countif(C[-1],RC[-1]),1,"""")" 'reference to 1st "helper" column (with all those "C[-1]")
.Value = .Value
.SpecialCells(xlCellTypeBlanks).EntireRow.Delete
.Offset(, -1).Resize(, 2).ClearContents ' reference to both "helper" columns: ".Offset(, -1)" reference the 1st since it shifts one column to the left from the one referenced in the preceeding "With.." statement (which is the 2nd column at thre right of data columns) and ".Resize(, 2)" enlarges to encose the adjacent column to the right
End With
End With
End With
End With
End Sub
it makes use of two "helper" columns, which I assumed could be the two adjacent to the last data columns (i.e.: if data columns are "A:D" then helper columns are "E:F")
should you need to use different "helper" columns then see comments about how they are located and change code accordingly

VBA array trouble error 9 script out of range

Thanks for reading my question,
I was given a list of about 250k entries along with names and sign in dates to accompany each entry to show when they logged. My task is to find out which users signed in on consecutive days, how often and how many times.
i.e. Bob smith had 3 consecutive days one time, 5 consecutive days 3 times.
joe smith had 8 consecutive days once, 5 consecutive days 8 times
etc
I am brand new to VBA and have been struggling to write a program to do this.
code:
Option Explicit
Option Base 1
Sub CountUUIDLoop()
Dim UUID As String
Dim Day As Date
Dim Instance() As Variant
ReDim Instance(50, 50)
Dim CountUUID As Variant
Dim q As Integer
Dim i As Long
Dim j As Long
Dim f As Integer
Dim g As Integer
Dim LastRow As String
f = 1
q = 1
g = 2
LastRow = Cells.Find("*", [A1], , , xlByRows, xlPrevious).Row
For i = q To LastRow
UUID = Cells(i, "A")
Instance(f, 1) = UUID
g = 2
For j = 1 To LastRow
If UUID = Cells(j, "A") Then
Instance(f, g) = Cells(j, "B")
g = g + 1
End If
Next j
f = f + 1
q = g - 1
Next i
End Sub
The goal of this code is to go through the entries and store them in the array 'Instance' such that the 2D array would look like [UUID1, B1, B2, B3]
[UUID2, B1, B2, B3, B4]
[UUID3, B1, B2]
Where the UUID is the user, the B1 represents the date that user signed in, b2 would be the next date they signed in etc. Some users have more or less dates than others.
My main issue has come with setting up the array as I keep getting different errors around it. I'm not sure how to define this 2D array partly because there will be over 30 000 rows, each with 1->85 columns.
Any help is appreciated, let me know if anything needs further clarification. Once again this is my first time using VBA so im sorry ahead of time if everything i've been doing is wrong.
P.S. I used ReDim Instance (50,50) as a test to see if i could make it work by predefining but same errors occurred. Thanks again!
As far as I understand from your question and code, you have a table with following structure:
..............A.................B
1........LOGIN1.......DATE1
2........LOGIN1.......DATE2
3........LOGIN1.......DATE3
4........LOGIN2.......DATE4
5........LOGIN2.......DATE5
6........LOGIN3.......DATE6
And your task in this code was to fetch data in a 2D structure like this:
RESULT_ARRAY-
............................|-LOGIN1-
............................................|-DATE1
............................................|-DATE2
............................................|-DATE3
............................|-LOGIN2-
............................................|-DATE4
............................................|-DATE5
............................|-LOGIN3-
............................................|-DATE6
First of all, you need to know what goes wrong in your code. Please see comments in code below to find out the reason of error:
Option Explicit
Option Base 1
Sub CountUUIDLoop()
Dim UUID As String
Dim Day As Date
Dim Instance() As Variant ' If you are using variant data type, it is not necesary to point it: default data type in VBA is Variant. Just write like this: "Dim Instance()"
ReDim Instance(50, 50) ' Limitation in 50 may be the reason, why your script is going into "out of range" error.
' Remember, that this operation means, that your array now will have following dimentions: [1..50,1..50]
Dim CountUUID As Variant 'Just write like this: "Dim CountUUID"
Dim q As Integer ' you can describe all your variables in one line, like this: "Dim q as Integer,f as Integer,g as Integer"
Dim i As Long
Dim j As Long
Dim f As Integer
Dim g As Integer
Dim LastRow As String ' first mistake: you are using String data type to perform numeric operations below in your FOR-cycle
f = 1 ' Your Instance array index starts from {0} and you are not using this index by starting from {1}.
q = 1 ' The reason to use this variable is not obvious. You could just use constant in FOR cycle below and avoid unnecessary variables.
g = 2 ' You could remove this line, because this var is set every time in cycle below (before second FOR)
LastRow = Cells.Find("*", [A1], , , xlByRows, xlPrevious).Row ' The alternative here is to use predefined Excel constants, like this:
' "Cells.SpecialCells(xlLastCell).Row".
'If LastRow is bigger, than {50} - this could be a reason of your Error.
For i = q To LastRow ' Here goes comparison between String and Integer data type, not good thing, but type conversion should work fine here.
UUID = Cells(i, "A") ' No need to perform re-set here, just move forward and assign value from this cell to the Instanse directly:
' Like this: Instance(f, 1) = Cells(i, "A")
Instance(f, 1) = UUID
g = 2
For j = 1 To LastRow ' It is another point, why "q" variable is not necessary. :)
If UUID = Cells(j, "A") Then ' You could use your Instansce value instead of UUID there, like this: "Instance(f, 1)"
Instance(f, g) = Cells(j, "B") 'If "g" variable will somehow become bigger, than {49}, this could become a reason of your Error.
g = g + 1
End If
Next j
f = f + 1
q = g - 1 ' "q" variable is not used after this row, so it is a strange unnecessary action
Next i
End Sub
Now, when we have some information about error, let me do some improvements on your code. I am certain, that to make most simply code, you can use your Excel worksheets to store and count data with VBA as background automations. But if you need the code with arrays, let's do this! :)
Option Explicit ' It is an option that turns on check for every used variable to be defined before execution. If this option is not defined, your code below will find undefined variables and define them when they are used. Good practice is to use this option, because it helps you, for example to prevent missprinting errors in variable names.
Option Base 1 ' This option sets the default index value for arrays in your code. If this option is not set, the default index value will be {0}.
Const HEADER_ROW = 1 ' It is a number to identify your header row, next row after this one will be counted as a row with data
Const UUID = 1 ' ID of element in our "Instance" array to store UUID
Const DATES_ID = 2 ' ID of element in our "Instance" array to store dates
Function CountUUIDLoop()
ActiveSheet.Copy After:=ActiveSheet 'Copy your worksheet to new one to ensure that source data will not be affected.
Dim Instance(), dates() ' "Instance" will be used to store all the data, "dates" will be used to store and operate with dates
ReDim Instance(2, 1) ' Set first limitation to the "Instance" array in style [[uuid, dates],id]
ReDim dates(1) ' Set first limitation to the "dates" array
Instance(DATES_ID, 1) = dates
Dim CountUUID
Dim i as Long, j as Long, f as Long, active_element_id As Long 'Integer is quite enough to perform our array manipulations, but Long datatype is recomended (please refer to UPDATE2 below)
i = HEADER_ROW + 1 ' Set first row to fetch data from the table
active_element_id = 1 ' Set first active element number
With ActiveSheet ' Ensure that we are working on active worksheet.
While .Cells(i, 1) <> "" 'If operated cell is not empty - continue search for data
If i > HEADER_ROW + 1 Then
active_element_id = active_element_id + 1 ' increment active element number
ReDim Preserve Instance(2, active_element_id) ' Assign new limitation (+ 1) for our Instances, don't forget to preserve our results.
ReDim dates(1) ' Set first limitation to the "dates" array
Instance(DATES_ID, active_element_id) = dates
End If
Instance(UUID, active_element_id) = .Cells(i, 1) ' save UUID
dates(1) = .Cells(i, 2) ' save first date
j = i + 1 ' Set row to search next date from as next row from current one.
While .Cells(j, 1) <> "" 'If operated cell is not empty - continue search for data
If .Cells(j, 1) = .Cells(i, 1) Then
ReDim Preserve dates(UBound(dates) + 1) ' Expand "dates" array, if new date is found.
dates(UBound(dates)) = .Cells(j, 2) ' Save new date value.
.Cells(j, 1).EntireRow.Delete 'Remove row with found date to exclude double checking in future
Else
j = j + 1 ' If uuid is not found, try next row
End If
Wend
Instance(DATES_ID, active_element_id) = dates
i = i + 1 'After all the dates are found, go to the next uuid
Wend
.Cells(i, 1) = "UUID COUNT" ' This will write you a "UUID COUNT" text in A column below all the rest of UUIDs on active worksheet
.Cells(i, 2) = i - HEADER_ROW - 1 ' This will write you a count of UUIDs in B column below all the rest of UUIDs on active worksheet
End With
CountUUIDLoop = Instance ' This ensures that your function (!) returns an array with all UUIDs and dates inside.
End Function
This function will print you count of your UUIDs at the bottom of active sheet and return you an array like this:
[[LOGIN1][1], [[DATE1][DATE2][DATE3]][1]]
I have used this order of storing data to avoid error with expanding of multidimentional arrays. This error is similar to yours, so you could read more about this there: How can I "ReDim Preserve" a 2D Array in Excel 2007 VBA so that I can add rows, not columns, to the array? Excel VBA - How to Redim a 2D array? ReDim Preserve to a Multi-Dimensional Array in Visual Basic 6
Anyway, you could use my function output ("Instance" array) to perform your further actions to find what you need or even display your uuid-dates values. :)
Good luck in your further VBA actions!
UPDATE
Here is the test procedure showing how to work with the above function's results:
Sub test()
Dim UUIDs ' The result of the "CountUUIDLoop" function will be stored there
Dim i as Long, j As Long ' Simple numeric variables used as indexies to run through our resulting array
UUIDs = CountUUIDLoop ' assign function result to a new variable
Application.DisplayAlerts = False ' Disable alerts from Excel
ActiveSheet.Delete ' Delete TMP worksheet
Application.DisplayAlerts = True ' Enable alerts from Excel
If UUIDs(UUID, 1) <> Empty Then ' This ensures that UUIDs array is not empty
Sheets.Add After:=ActiveSheet ' Add new worksheet after active one to put data into it
With ActiveSheet 'Ensure that we are working with active worksheet
.Cells(HEADER_ROW, 1) = "UUIDs/dates" ' Put the header into the "HEADER_ROW" row
For i = 1 To UBound(UUIDs, 2) ' run through all the UUIDs
.Cells(1 + HEADER_ROW, i) = UUIDs(UUID, i) ' Put UUID under the header
For j = 1 To UBound(UUIDs(DATES_ID, i)) ' run through all the dates per UUID
.Cells(j + 1 + HEADER_ROW, i) = UUIDs(DATES_ID, i)(j) ' put date into column below the UUID
Next j ' Go to next date
Next i ' Go to next UUID
.Cells.EntireColumn.AutoFit ' This will make all columns' width to fit its contents
End With
Else
MsgBox "No UUIDs are found!", vbCritical, "No UUIDs on worksheet" ' Show message box if there are no UUIDs in function result
End If
End Sub
So, if you'll have following data on the active worksheet:
..............A.................B
1........LOGIN1.......DATE1
2........LOGIN1.......DATE2
3........LOGIN1.......DATE3
4........LOGIN2.......DATE4
5........LOGIN2.......DATE5
6........LOGIN3.......DATE6
...this sub will put UUIDs on the new sheet like this:
..............A.................B.................C
1........UUIDs/dates
2........LOGIN1........LOGIN2........LOGIN3
3........DATE1.........DATE4.........DATE6
4........DATE2.........DATE5
5........DATE3
UPDATE2
It is recomended to use Long data type instead of Integer each type when integer (or whole number) variable is needed. Long is slightly faster, it has much wider limitations and costs no additional memory. Here is proof link:
MSDN:The Integer, Long, and Byte Data Types
I would recommend using collections and a dictionary instead of arrays. The below code will structure the data in a way that is very similar to the way you wanted it.
Sub collect_logins_by_user_()
'you need to enable the microsoft scripting runtime
'in tools - references
'assuming unique ids are in col A and there are no gaps
'and assuming dates in col B and there are no gaps
'
'The expected runtime for this is O(n) and I have used similar code on more than 250.000 record.
'It still takes a while obviously, but should run just fine.
'
'The the data will bestructed in the following format:
'{id_1: [d_1, d_2,...], id_2: [d_3, d_4,...], ...}
Dim current_id As Range: Set current_id = ActiveSheet.Range("A2") 'modify range as required
Dim logins_by_users As New Dictionary
While Not IsEmpty(current_id)
If Not logins_by_users.Exists(current_id.Value) Then
Set logins_by_users(current_id.Value) = New Collection
End If
logins_by_users(current_id.Value).Add current_id.Offset(ColumnOffset:=1).Value
Set current_id = current_id.Offset(RowOffset:=1)
Wend
'Once you have the data structured, you can do whatever you want with it.
'like printing it to the immediate window.
Dim id_ As Variant
For Each id_ In logins_by_users
Debug.Print "======================================================="
Debug.Print id_
Dim d As Variant
For Each d In logins_by_users(id_)
Debug.Print d
Next d
Next id_
Debug.Print "======================================================="
End Sub
I have written a bit of code that does something along the lines of what you are trying to do - it prints to the debug window the different numbers of consecutive logs per user, separeted by commas.
This code makes use of the dictionary object - which essentially is an associative array where the indexes are not restrained to numbers like they are in arrays, and offers a couple of convenient features to manipulate data that arrays don't.
I have tested this on a sheet including user ids in colomn A and log dates in column B - including headers - and this looks to work fine. Fell free to give it a try
Sub mysub()
Dim dic As Object
Dim logs As Variant
Dim myval As Long
Dim mykey As Variant
Dim nb As Long
Dim i As Long
Set dic = CreateObject("Scripting.dictionary")
'CHANGE TO YOUR SHEET REFERENCE HERE
For Each cell In Range(Cells(2, 1), Cells(Worksheets("Sheet8").Rows.count, 1).End(xlUp))
mykey = cell.Value
myval = cell.Offset(0, 1)
If myval <> 0 Then
On Error GoTo ERREUR
dic.Add mykey, myval
On Error GoTo 0
End If
Next cell
For Each Key In dic
logs = Split(dic(Key), ",")
logs = sortArray(logs)
i = LBound(logs) + 1
nb = 1
Do While i <= UBound(logs)
Do While CLng(logs(i)) = CLng(logs(i - 1)) + 1
nb = nb + 1
i = i + 1
Loop
If nb > 1 Then
tot = tot & "," & CStr(nb)
nb = 1
End If
i = i + 1
Loop
If tot <> "" Then dic(Key) = Right(tot, Len(tot) - 1)
Debug.Print "User: " & Key & " - Consecutive logs: " & dic(Key)
tot = ""
mys = ""
Next Key
Exit Sub
ERREUR:
If myval <> 0 Then dic(mykey) = dic(mykey) & "," & CStr(myval)
Resume Next
End Sub
Function sortArray(a As Variant) As Variant
For i = LBound(a) + 1 To UBound(a)
j = i
Do While a(j) < a(j - 1)
temp = a(j - 1)
a(j - 1) = a(j)
a(j) = temp
j = j - 1
If j = 0 Then Exit Do
Loop
Next i
sortArray = a
End Function

Resources