I have a huge challenge which is: Search a massive database, a list of products. This database is divided into 3 dif. sheets. The result should be stored on a 4th sheet and organized by date in columns and summarized by quantity. Each database sheet has the same format (Product / Date / Quantity).
I was told that a vba array would work perfectly.
Any help will be very appreciated.
Thanksenter image description here
Pivot (Multiple Worksheets)
The Code
Option Explicit
Sub PivotMulti()
' Data
Const srcList As String = "Sheet1,Sheet2,Sheet3"
Const srcFirst As String = "A2"
' Destination
Const dstName As String = "SUMMARY"
Const dstFirst As String = "B2"
Const TitlesList As String = "Source Data,Product"
' Other
Const Delimiter As String = "###"
' Workbook
Dim wb As Workbook: Set wb = ThisWorkbook
' Define Data Ranges and write their values to arrays of Data Array.
Dim srcNames() As String: srcNames = Split(srcList, ",")
Dim sUpper As Long: sUpper = UBound(srcNames)
Dim Data As Variant: ReDim Data(0 To sUpper)
Dim rng As Range
Dim dCount As Long
Dim n As Long
For n = 0 To sUpper
With wb.Worksheets(srcNames(n)).Range(srcFirst)
Set rng = .Resize(.Worksheet.Rows.Count - .Row + 1).Find( _
What:="*", _
LookIn:=xlFormulas, _
SearchDirection:=xlPrevious)
Data(n) = .Resize(rng.Row - .Row + 1, 3).Value
dCount = dCount + UBound(Data(n))
End With
Next n
' Define data structures.
Dim dictS As Object: Set dictS = CreateObject("Scripting.Dictionary")
dictS.CompareMode = vbTextCompare
Dim dictD As Object: Set dictD = CreateObject("Scripting.Dictionary")
Dim arlS As Object: Set arlS = CreateObject("System.Collections.ArrayList")
Dim arlD As Object: Set arlD = CreateObject("System.Collections.ArrayList")
' Declare additional variables.
Dim CurrString As String
Dim CurrDate As Date
Dim i As Long
Dim j As Long
Dim k As Long
' Write values from Data Array to dictionaries and array lists
' (dictianaries for 'unique' and 'sum', array lists for 'sort').
For n = 0 To sUpper
For i = 1 To UBound(Data(n), 1)
CurrDate = Data(n)(i, 2)
If Not dictD.Exists(CurrDate) Then
arlD.Add CurrDate
dictD(CurrDate) = Empty
End If
CurrString = Data(n)(i, 1) & Delimiter & srcNames(n) & Delimiter _
& Format(CurrDate, "yyyymmdd") & Delimiter & CurrDate
dictS(CurrString) = dictS(CurrString) + Data(n)(i, 3)
Next i
Next n
Set dictD = Nothing
Erase Data
Erase srcNames
arlD.Sort
Dim Key As Variant
For Each Key In dictS.Keys
arlS.Add Key & Delimiter & dictS(Key)
Next Key
Set dictS = Nothing
arlS.Sort
' Define Result Array.
Dim rrCount As Long: rrCount = 1 + arlS.Count
Dim rcCount As Long: rcCount = 2 + arlD.Count
Dim Result As Variant: ReDim Result(1 To rrCount, 1 To rcCount)
' Write headers.
Dim Titles() As String: Titles = Split(TitlesList, ",")
Result(1, 1) = Titles(0)
Result(1, 2) = Titles(1)
Erase Titles
j = 2
For Each Key In arlD
j = j + 1
Result(1, j) = Key
Next Key
' Write 'body'.
Dim Current() As String
Dim Previous(0 To 1) As String
i = 1
For Each Key In arlS
i = i + 1
Current = Split(Key, Delimiter)
If Current(0) <> Previous(0) Or Current(1) <> Previous(1) Then
For j = 1 To 2
Result(i, j) = Current(2 - j)
Next j
Else
i = i - 1
End If
CurrDate = CDate(Current(3))
Result(i, 3 + arlD.IndexOf(CurrDate, 0)) = CDbl(Current(4))
Previous(0) = Current(0)
Previous(1) = Current(1)
Next Key
Set arlD = Nothing
Set arlS = Nothing
' Write values from Result Array to Destination Range.
With wb.Worksheets(dstName).Range(dstFirst).Resize(, rcCount)
Application.ScreenUpdating = False
' Worksheet
.Worksheet.Cells.Clear
' Range
With .Resize(i)
.Value = Result ' Write.
With .Borders
.LineStyle = xlContinuous
.Weight = xlThin
End With
End With
' Headers
.Font.Bold = True
.Interior.Color = vbYellow
' Body
With .Resize(i - 1).Offset(1)
'.Font.Color = vbRed
End With
' Columns
.Columns.AutoFit
Application.ScreenUpdating = True
End With
MsgBox "Data transferred.", vbInformation, "Success"
End Sub
Related
I am looking for if it is possible to get the data and headers from a table as in the example image and have the output go to two columns with the first column being a repeating header? I did try the transpose however the email row kept populating up to column E.
Please, try the next way. It uses arrays being fast even for large ranges, mostly working in memory. It returns starting from "F2". It is able to process any other columns you (may) need, after "Status":
Sub TransposeMails()
Dim sh As Worksheet, lastR As Long, lastCol As Long
Dim arrH, arr, arrFin, i As Long, j As Long, k As Long
Set sh = ActiveSheet 'use here the necessary sheet
lastR = sh.Range("A" & sh.rows.count).End(xlUp).row 'last row
lastCol = sh.cells(1, sh.Columns.count).End(xlToLeft).column 'last column
arrH = Application.Transpose(sh.Range(sh.cells(1, 1), sh.cells(1, lastCol)).Value2) 'place headers in an array
arr = sh.Range("A2", sh.cells(lastR, lastCol)).Value2 'place the range to be processed (except headers) in an array for faster iteration/processing
ReDim arrFin(1 To (UBound(arrH) + 1) * UBound(arr), 1 To 2) 'Redim the final array (keeping the processed result)
'+ 1 for the empty rows in between...
For i = 1 To UBound(arr)
For j = 1 To UBound(arrH)
k = k + 1
arrFin(k, 1) = arrH(j, 1): arrFin(k, 2) = arr(i, j)
Next j
k = k + 1 'for the empty row between groups...
Next i
'drop the processed array content:
sh.Range("G2").Resize(UBound(arrFin), 2).Value2 = arrFin
End Sub
The code can be easily adapted to return anywhere (another sheet, workbook, range etc).
The range to be processed must start from "A1" ("Email" header) and not having any other record after the last header (on the first row)...
Transpose Data
Sub TransposeData()
Const SRC_NAME As String = "Sheet1"
Const DST_NAME As String = "Sheet1"
Const DST_FIRST_CELL As String = "A8"
Const EMPTY_COLS As Long = 0
Const EMPTY_ROWS As Long = 1
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
Dim sws As Worksheet: Set sws = wb.Sheets(SRC_NAME)
Dim srg As Range: Set srg = sws.Range("A1").CurrentRegion
Dim drOffset As Long: drOffset = srg.Columns.Count + EMPTY_ROWS
Dim dcOffset As Long: dcOffset = 1 + EMPTY_COLS
Dim dws As Worksheet: Set dws = wb.Sheets(DST_NAME)
Dim dfCell As Range: Set dfCell = dws.Range(DST_FIRST_CELL)
Application.ScreenUpdating = False
Dim srrg As Range, shrg As Range
Dim IsHeaderReady As Boolean, IsFirstRowDone As Boolean
For Each srrg In srg.Rows
If Not IsHeaderReady Then
srrg.Copy
dfCell.PasteSpecial Transpose:=True
Set shrg = dfCell.Resize(srg.Columns.Count) ' transpose no more
IsHeaderReady = True
Else ' header is ready; it's already copied for the first data row
If IsFirstRowDone Then shrg.Copy dfCell Else IsFirstRowDone = True
srrg.Copy
dfCell.Offset(, dcOffset).PasteSpecial Transpose:=True
Set dfCell = dfCell.Offset(drOffset)
End If
Next srrg
Application.ScreenUpdating = True
MsgBox "Data transposed.", vbInformation
End Sub
If I understand you correctly
Sub test()
'set the range of the header as rg variable
'count how many data under EMAIL header as cnt variable
Dim rg As Range: Set rg = Range("A1", Range("A1").End(xlToRight))
Dim cnt As Integer: cnt = Range(rg, rg.End(xlDown)).Rows.Count - 1
Dim i As Integer: Dim rslt As Range
'loop to each range with data as many as the cnt value
'prepare the result range as rslt variable
'put the value of header name to rslt range
'put the looped range value to rslt.offset(0,1)
For i = 1 To cnt
Set rslt = Range("A" & Rows.Count).End(xlUp).Offset(3, 0) _
.Resize(rg.Columns.Count, 1)
rslt.Value = Application.Transpose(rg)
rslt.Offset(0, 1).Value = Application.Transpose(rg.Offset(i, 0))
Next
End Sub
Please note, the code must be run where the sheet contains the data is active.
I want to use the vba sumifs array and scripting.dictionary because there are a hundred thousand records there may be the best solution. For information sheet "DBALL" is the source and sheet "RECON" is the result. I also found the vba code below but it doesn't match the result.
info formula sheet "RECON" column B "In" = SUMIFS(DBALL!$A$2:$A$5,DBALL!$C$2:$C$5,RECON!$A2,DBALL!$B$2:$B$5,RECON!B$1)
info formula sheet "RECON" column c "Out" = SUMIFS(DBALL!$A$2:$A$5,DBALL!$C$2:$C$5,RECON!$A2,DBALL!$B$2:$B$5,RECON!C$1)
info formula sheet "RECON" column d "difference" = B2-C2
Thanks
Sub SUMIFSFASTER()
Dim arr, ws, rng As Range, keyCols, valueCol As Long, destCol As Long, i As Long, frm As String, sep As String
Dim t, dict, arrOut(), arrValues(), v, tmp, n As Long
keyCols = Array(2, 3) 'these columns form the composite key
valueCol = 1 'column with values (for sum)
destCol = 4 'destination for calculated values
t = Timer
Set ws = Sheets("DBALL")
Set rng = ws.Range("A1").CurrentRegion
n = rng.Rows.Count - 1
Set rng = rng.Offset(1, 0).Resize(n) 'exclude headers
'build the formula to create the row "key"
For i = 0 To UBound(keyCols)
frm = frm & sep & rng.Columns(keyCols(i)).Address
sep = "&""|""&"
Next i
arr = ws.Evaluate(frm) 'get an array of composite keys by evaluating the formula
arrValues = rng.Columns(valueCol).Value 'values to be summed
ReDim arrOut(1 To n, 1 To 1) 'this is for the results
Set dict = CreateObject("scripting.dictionary")
'first loop over the array counts the keys
For i = 1 To n
v = arr(i, 1)
If Not dict.exists(v) Then dict(v) = Array(0, 0) 'count, sum
tmp = dict(v) 'can't modify an array stored in a dictionary - pull it out first
tmp(0) = tmp(0) + 1 'increment count
tmp(1) = tmp(1) + arrValues(i, 1) 'increment sum
dict(v) = tmp 'return the modified array
Next i
'second loop populates the output array from the dictionary
For i = 1 To n
arrOut(i, 1) = dict(arr(i, 1))(1) 'sumifs
'arrOut(i, 1) = dict(arr(i, 1))(0) 'countifs
'arrOut(i, 1) = dict(arr(i, 1))(1) / dict(arr(i, 1))(0) 'averageifs
Next i
'populate the results
rng.Columns(destCol).Value = arrOut
Debug.Print "Checked " & n & " rows in " & Timer - t & " secs"
End Sub
Source
RESULT
As said in the comments a better solution is probably to use a pivot table resp. power pivot.
If you are after a solution with VBA and want to use a dictionary I would probably use the following code.
First you need to create a class cVal which stores the values you are after
Option Explicit
Public qtyIn As Double
Public qtyOut As Double
Then you can use the following code
Option Explicit
Sub useDict()
Const COL_VAL = 1
Const COL_INOUT = 2
Const COL_COMBINE = 3
Const GRO_IN = "IN"
Const GRO_OUT = "OUT"
Dim rg As Range, ws As Worksheet
' Get the range with the data
Set ws = Worksheets("DBALL")
Set rg = ws.Range("A1").CurrentRegion
Set rg = rg.Offset(1, 0).Resize(rg.Rows.Count - 1)
Dim vDat As Variant
vDat = rg
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
Dim Key As Variant, gro As Variant
Dim i As Long, sngVal As cVal
For i = LBound(vDat, 1) To UBound(vDat, 1)
' Key of the dictionary
Key = vDat(i, COL_COMBINE)
' trim the value and do not consider upper/lower case
gro = UCase(Trim(vDat(i, COL_INOUT)))
If dict.Exists(Key) Then
' just increase the "member" values of the already stored object
Set sngVal = dict(Key)
With sngVal
If gro = GRO_IN Then
.qtyIn = .qtyIn + vDat(i, COL_VAL)
End If
If gro = GRO_OUT Then
.qtyOut = .qtyOut + vDat(i, COL_VAL)
End If
End With
Else
' Create a new object which stores the summed values for "IN" resp "OUT"
Set sngVal = New cVal
With sngVal
If gro = GRO_IN Then
.qtyIn = vDat(i, COL_VAL)
End If
If gro = GRO_OUT Then
.qtyOut = vDat(i, COL_VAL)
End If
End With
dict.Add Key, sngVal
End If
Next i
' write Dictionary
' put the values of the dictionary in an array
' this is faster than writing each single line directly to the sheet
ReDim vDat(1 To dict.Count, 1 To 4)
i = 1
For Each Key In dict.Keys
vDat(i, 1) = Key
vDat(i, 2) = dict(Key).qtyIn
vDat(i, 3) = dict(Key).qtyOut
vDat(i, 4) = Abs(dict(Key).qtyIn - dict(Key).qtyOut)
i = i + 1
Next Key
'write Header
Set rg = Worksheets("RECON").Range("A1")
Set rg = rg.Resize(, 4)
rg.Clear
rg = Array("COMBINE", "In", "Out", "Diff")
Set rg = Worksheets("RECON").Range("A2")
Set rg = rg.Resize(dict.Count, 4)
rg.Clear
rg = vDat
' PS Code to add a sum row below the data
Set rg = Worksheets("RECON").Range("A" & dict.Count + 2)
Set rg = rg.Resize(1, 4)
rg.Clear
'rg.Columns(1).Value = "Total"
Dim bSum As Double, rDat As Variant
rDat = Application.Index(vDat, , 2)
bSum = WorksheetFunction.sum(rDat)
rg.Columns(2).Value = bSum
rDat = Application.Index(vDat, , 3)
bSum = WorksheetFunction.sum(rDat)
rg.Columns(3).Value = bSum
rDat = Application.Index(vDat, , 4)
bSum = WorksheetFunction.sum(rDat)
rg.Columns(4).Value = bSum
End Sub
But I doubt that to be faster than a Pivot Table
I am trying to find a way to average an array column value with a condition on items from another column in that array - I am aware that a class or dictionary might be the best solution but I would like to stick to an array as in my real scenario I have to use an array.
In this case the data is as follows
Risk ID Data set 1 Data set 2
23359720 1154 587
23359720 1254 658
23359720 854 756
23293773 965 1456
20053692 1458 458
I would like to find the average of Data sets 1 and 2 per Risk ID, here is what I've tried but does not work - I have seen that this it's not possible to use for each and point it to a specific column, but not sure what else to do in the case of an array?
Edit: expected result data:
ArrayResultAverage()
Risk ID Avg Data set 1 Avg Data set 2
23359720 1087.33 667
23293773 965 1456
20053692 1458 458
Sub Test_Arr_Avg()
'
Dim TESTWB As Workbook
Dim TESTWS As Worksheet
Set TESTWB = ThisWorkbook
Set TESTWS = TESTWB.Worksheets("TEST")
'Array set up
Dim RngTest As Range
Dim ArrTestAvg As Variant
NbRowsTest = TESTWS.Range("A1").End(xlDown).Row
Set RngTest = TESTWS.Range(TESTWS.Cells(1, 1), TESTWS.Cells(NbRowsTest, 3))
ArrTestAvg = RangeToArray2D(RngTest)
'Find the average of Data Range 1 for each item in Risk ID
For k = 1 To UBound(ArrTestAvg, 1)
Dim Sum As Variant
Sum = 0
For Each Item In ArrTestAvg(k, 1)
Sum = Sum + ArrTestAvg(k, 2)
Dim AverageDataSet1 As Variant
AverageDataSet1 = Sum / UBound(ArrTestAvg(Item)) + 1
Debug.Print AverageDataSet1
Next Item
Next k
End Sub
Public Function RangeToArray2D(inputRange As Range) As Variant()
Dim size As Integer
Dim inputValue As Variant, outputArray() As Variant
inputValue = inputRange
On Error Resume Next
size = UBound(inputValue)
If Err.Number = 0 Then
RangeToArray2D = inputValue
Else
On Error GoTo 0
ReDim outputArray(1 To 1, 1 To 1)
outputArray(1, 1) = inputValue
RangeToArray2D = outputArray
End If
On Error GoTo 0
End Function
Get Averages of Unique Data
Adjust the values in the constants section, especially the destination worksheet name (it's the same as the source worksheet name) and its first cell address.
The dictionary's keys hold the unique risk ids, while its items (values) hold the associated destination rows.
The result is written to the same array (which is too big) but with dr the destination row size is tracked and only three columns will be copied.
Before the calculation of the averages, column 1 holds the unique risk ids (the same order as in the dictionary), columns 2 and 3 hold the sums while columns 4 and 5 hold the counts of the first and second data set respectively.
Option Explicit
Sub Test_Arr_Avg()
' Source
Const sName As String = "Sheet1"
' Destination
Const dName As String = "Sheet1"
Const dFirstCellAddress As String = "E1"
' Workbook
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
' Read from source.
Dim sws As Worksheet: Set sws = wb.Worksheets(sName)
Dim slRow As Long: slRow = sws.Cells(sws.Rows.Count, "A").End(xlUp).Row
Dim srg As Range: Set srg = sws.Range("A1", sws.Cells(slRow, "C"))
Dim srCount As Long: srCount = srg.Rows.Count
' Write source range values to array.
Dim Data As Variant: Data = GetRange(srg)
' Add two helper columns for the count.
ReDim Preserve Data(1 To srCount, 1 To 5)
' Sum up and count uniques.
Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
dict.CompareMode = vbTextCompare
Dim dr As Long: dr = 1 ' first row are headers
Dim sr As Long
Dim cr As Long
Dim c As Long
For sr = 2 To srCount
' Sum up.
If dict.Exists(Data(sr, 1)) Then
cr = dict(Data(sr, 1))
For c = 2 To 3
Data(cr, c) = Data(cr, c) + Data(sr, c)
Next c
Else
dr = dr + 1
cr = dr
dict(Data(sr, 1)) = cr
For c = 1 To 3
Data(cr, c) = Data(sr, c)
Next c
End If
' Count.
For c = 4 To 5
Data(cr, c) = Data(cr, c) + 1
Next c
Next sr
' Calculate averages.
For cr = 2 To dr
For c = 2 To 3
Data(cr, c) = Data(cr, c) / Data(cr, c + 2)
' You might want to round the results instead:
'Data(cr, c) = Round(Data(cr, c) / Data(cr, c + 2), 2)
Next c
Next cr
Application.ScreenUpdating = False
' Write to destination.
Dim dws As Worksheet: Set dws = wb.Worksheets(dName)
With dws.Range(dFirstCellAddress).Resize(, 3)
.Resize(dr).Value = Data
' Clear below.
.Resize(dws.Rows.Count - .Row - dr + 1).Offset(dr).Clear
' Apply various formatting.
.Font.Bold = True ' headers
.Resize(dr - 1, 2).Offset(1, 1).NumberFormat = "#0.00" ' averages
.EntireColumn.AutoFit ' columns
End With
'wb.Save
' Inform.
Application.ScreenUpdating = True
MsgBox "Risk ids averaged.", vbInformation
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns the values of a range ('rg') in a 2D one-based array.
' Remarks: If ˙rg` refers to a multi-range, only its first area
' is considered.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetRange( _
ByVal rg As Range) _
As Variant
Const ProcName As String = "GetRange"
On Error GoTo ClearError
If rg.Rows.Count + rg.Columns.Count = 2 Then ' one cell
Dim Data As Variant: ReDim Data(1 To 1, 1 To 1): Data(1, 1) = rg.Value
GetRange = Data
Else ' multiple cells
GetRange = rg.Value
End If
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
It would be complicated to use a single Dictionary. Here I add a Sub-Dictionary for each Risk ID to the main Dictionary. The Sub-Dictionary is used to hold all the values for each ID. The final step is to create an array of averages for all the main Dictionary items.
Sub Test_Arr_Avg()
Dim Data As Variant
With TestWS.Range("A1").CurrentRegion
Data = .Offset(1).Resize(.Rows.Count - 1, 3)
End With
Dim Results As Variant
Results = KeyedAverages(Data, 1, 2)
Stop
End Sub
Function KeyedAverages(Data As Variant, IDColumn As Long, ValueColumn As Long)
Dim Map As Object
Set Map = CreateObject("Scripting.Dictionary")
Dim Key As Variant
Dim r As Long
For r = 1 To UBound(Data)
Key = CStr(Data(r, IDColumn))
If Len(Key) > 0 Then
If Not Map.Exists(Key) Then Map.Add Key, CreateObject("Scripting.Dictionary")
With Map(Key)
.Add CStr(.Count), Data(r, ValueColumn)
End With
End If
Next
Dim Results As Variant
Dim Values As Variant
ReDim Results(1 To Map.Count, 1 To 2)
Dim n As Long
For Each Key In Map.Keys
n = n + 1
Values = Map(Key).Items
Results(n, 1) = Key
Results(n, 2) = WorksheetFunction.Average(Values)
Next
KeyedAverages = Results
End Function
Public Function TestWB() As Workbook
Set TestWB = ThisWorkbook
End Function
Public Function TestWS() As Worksheet
Set TestWS = TestWB.Worksheets("Test")
End Function
Note: Code updated to exclude empty items.
I have a large database (~7000 rows x 25 columns) that i have assigned to an array. Based on user inputs, I am trying to search the database to find items that match the input and copy the entire row to a new array to create a new database to filter with the next question. The arrays are DBRange and DBT and are defined as public variants. I've tried copying the data from DBRange to a new sheet, but that is incredibly slow and I'm trying to speed things up with keeping things within arrays if possible.
DBRange = wsd.Range("A1").CurrentRegion 'Sets DBRange to the entirety of the Database
Cervical = 0
If CervicalStartOB.Value = True Then
Cervical = 1
SpineSection.Hide
For i = LBound(DBRange, 1) To UBound(DBRange, 1) 'starts for loop starting with the 1st row in the array to the last row
If DBRange(i, 13) = "X" Then 'determines if the value in row i column 13 has an X
ReDim Preserve DBT(count, UBound(DBRange, 2))
DBT(count, UBound(DBRange, 2)) = Application.WorksheetFunction.Index(DBRange, i, 0)
count = count + 1
End If
Next i
Get Range Criteria Rows
Option Explicit
Sub GetRangeCriteriaRowsTESTKeepHeaders()
Const sCriteriaString As String = "X"
Const sCol As Long = 13
Const sfRow As Long = 2 ' First Data Row
Dim wsd As Worksheet: Set wsd = ActiveSheet
' Reference the source range
Dim srg As Range: Set srg = wsd.Range("A1").CurrentRegion
' Write the criteria rows to an array.
' If you want to keep headers, use the first row ('sfrow')
' as the parameter of the 'FirstRow' argument of the function.
Dim DBT As Variant
DBT = GetRangeCriteriaRows(srg, sCriteriaString, sCol, sfRow)
If IsEmpty(DBT) Then Exit Sub
End Sub
Sub GetRangeCriteriaRowsTESTOnlyData()
Const sCriteriaString As String = "X"
Const sCol As Long = 13
Const sfRow As Long = 2 ' First Data Row
Dim wsd As Worksheet: Set wsd = ActiveSheet
' Reference the source range.
Dim srg As Range: Set srg = wsd.Range("A1").CurrentRegion
' Reference the data range (no headers).
Dim shrCount As Long: shrCount = sfRow - 1
Dim sdrg As Range
Set sdrg = srg.Resize(srg.Rows.Count - shrCount).Offset(shrCount)
' Write the criteria rows to an array.
' If the range has no headers, don't use the 'FirstRow' argument
' of the function.
Dim DBT As Variant: DBT = GetRangeCriteriaRows(sdrg, sCriteriaString, sCol)
If IsEmpty(DBT) Then Exit Sub
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose: Returns the values of the rows of a range ('SourceRange'),
' that meet a string criterion ('CriteriaString') in a column
' ('CriteriaColumnIndex'), in a 2D one-based array.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function GetRangeCriteriaRows( _
ByVal SourceRange As Range, _
ByVal CriteriaString As String, _
Optional ByVal CriteriaColumnIndex As Long = 1, _
Optional ByVal FirstRow As Long = 1) _
As Variant
Const ProcName As String = "GetRangeCriteriaRows"
On Error GoTo ClearError
' Count the source rows and the source/destination columns.
Dim srCount As Long: srCount = SourceRange.Rows.Count
Dim cCount As Long: cCount = SourceRange.Columns.Count
' Count the source header rows.
Dim shrCount As Long: shrCount = FirstRow - 1
' Define the source data range according to the first row.
Dim sdrg As Range
Set sdrg = SourceRange.Resize(srCount - shrCount).Offset(shrCount)
' Write the source range values to the source array.
Dim sData As Variant: sData = SourceRange.Value
' Count the criteria rows in the source data criteria column range.
Dim sdcrg As Range: Set sdcrg = sdrg.Columns(CriteriaColumnIndex)
Dim drCount As Long
drCount = Application.CountIf(sdcrg, CriteriaString) + shrCount
' Define the destination array.
Dim dData As Variant: ReDim dData(1 To drCount, 1 To cCount)
Dim sr As Long ' Current Source Row
Dim c As Long ' Current Source/Destination Column
Dim dr As Long ' Current Destination Row
' Write the header rows from the source array to the destination array.
If FirstRow > 1 Then
For sr = 1 To shrCount
dr = dr + 1
For c = 1 To cCount
dData(dr, c) = sData(sr, c)
Next c
Next sr
End If
' Write the criteria rows from the source array to the destination array.
For sr = FirstRow To srCount
If StrComp(CStr(sData(sr, CriteriaColumnIndex)), CriteriaString, _
vbTextCompare) = 0 Then
dr = dr + 1
For c = 1 To cCount
dData(dr, c) = sData(sr, c)
Next c
End If
Next sr
GetRangeCriteriaRows = dData
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
I am trying to work through a code that utilizes a system to check two different worksheets by using a for loop and highlight the differences/edits made in the second sheet ("Version 2") onto the first sheet ("Original"). I have a feeling that I need to utilize an array but I'm not advanced enough where I know how to store the values and then later write them onto another sheet (down below).
I've gotten the code so that it highlights all the relevant cells, but now I'm trying to output it into a report (on another sheet called 'Logged Changes') which will summarize all the cell addresses where edits were made. Please forgive all the variables as this is from an old code set where variables are not explicitly defined:
Private Sub CompareBasic()
Dim actSheet As Range
Dim k As Integer
Dim o As Long
Dim p As Long
Dim i As Integer
Dim change As Integer
o = Worksheets("Original").Cells(2, Columns.Count).End(xlToLeft).Column
p = Worksheets("Original").Range("A" & Rows.Count).End(xlUp).Row
change = 0
Sheets("Original").Select
For i = 2 To p
For k = 1 To o
If IsNumeric(Worksheets("Original").Cells(i, k).Value) = True Then
If Worksheets("Original").Cells(i, k).Value <> Worksheets("Version 2").Cells(i, k).Value Then
Worksheets("Original").Cells(i, k).Interior.ColorIndex = 37
change = change + 1
End If
Else
If StrComp(Worksheets("Original").Cells(i, k), Worksheets("Version 2").Cells(i, k), vbBinaryCompare) <> 0 Then
Worksheets("Original").Cells(i, k).Interior.ColorIndex = 37
change = change + 1
End If
End If
Next k
Next i
Unload Me
MsgBox "Number of cells edited counted: " & change, vbOKOnly + vbExclamation, "Summary"
b = Empty
answer = MsgBox("Do you want to run the Report?", vbYesNo + vbQuestion)
If answer = vbYes Then
If Sheet_Exists("Logged Changes") = False Then
Sheet_Name = "Logged Changes"
Worksheets.Add(After:=Sheets(Sheets.Count)).Name = Sheet_Name
End If
Worksheets("Logged Changes").Range("A1") = "Edited Requirements"
Else
Unload Me
End If
End Sub
I have tried fiddling around with the code, but didn't want to clog it up with any unnecessary/broken lines. Any help would be greatly appreciated!
Try this:
Option Explicit
Private Sub CompareBasic()
Const SHT_REPORT As String = "Logged Changes"
Dim actSheet As Range
Dim c As Integer
Dim o As Long
Dim p As Long
Dim r As Long
Dim change As Long, wsOrig As Worksheet, wsNew As Worksheet, wsReport As Worksheet
Dim dataOrig, dataNew, rngData As Range, v1, v2, bDiff As Boolean
Dim arrUpdates
Set wsOrig = Worksheets("Original")
Set wsNew = Worksheets("Version 2")
o = wsOrig.Cells(2, Columns.Count).End(xlToLeft).Column
p = wsOrig.Range("A" & Rows.Count).End(xlUp).Row
Set rngData = wsOrig.Range("A2", wsOrig.Cells(p, o))
dataOrig = rngData.Value 'get an array of data
dataNew = wsNew.Range(rngData.Address).Value 'array of new data
ReDim arrUpdates(1 To rngData.Cells.Count, 1 To 3) 'for change info
change = 0
For r = 1 To UBound(dataOrig, 1)
For c = 1 To UBound(dataOrig, 2)
v1 = dataOrig(r, c)
v2 = dataNew(r, c)
If Len(v1) > 0 Or Len(v2) > 0 Then
If IsNumeric(v1) Then
bDiff = v1 <> v2
Else
bDiff = StrComp(v1, v2, vbBinaryCompare) <> 0
End If
End If
'any difference?
If bDiff Then
change = change + 1
With rngData.Cells(r, c)
arrUpdates(change, 1) = .Address
.Interior.ColorIndex = 37
End With
arrUpdates(change, 2) = v1
arrUpdates(change, 3) = v2
End If
Next c
Next r
If MsgBox("Do you want to run the Report?", vbYesNo + vbQuestion) = vbYes Then
With GetSheet(SHT_REPORT, ThisWorkbook)
.UsedRange.ClearContents
.Range("A1") = "Edited Requirements"
.Range("A3").Resize(1, 3).Value = Array("Address", wsOrig.Name, wsNew.Name)
.Range("A4").Resize(change, 3).Value = arrUpdates
End With
Else
'Unload Me
End If
End Sub
'return as sheet from wb by name (and create it if it doesn't exist)
Function GetSheet(wsName, wb As Workbook) As Worksheet
Dim rv As Worksheet
On Error Resume Next
Set rv = wb.Worksheets(wsName)
On Error GoTo 0
If rv Is Nothing Then
Set rv = wb.Worksheets.Add(After:=wb.Worksheets(wb.Worksheets.Count))
rv.Name = "Logged Changes"
End If
Set GetSheet = rv
End Function
Sheet Differences
Option Explicit
Sub logChanges()
Const ws1Name As String = "Original"
Const ws2Name As String = "Version 2"
Const wsResult As String = "Logged Changes"
Const FirstRow As Long = 2
Const FirstColumn As Long = 1
Const LastRowColumn As Long = 1
Const LastColumnRow As Long = 2
Const ResultFirstCell As String = "A2"
Dim Headers As Variant
Headers = Array("Id", "Address", "Original", "Version 2")
Dim wb As Workbook: Set wb = ThisWorkbook
Dim ws As Worksheet: Set ws = wb.Worksheets(ws1Name)
Dim LastRow As Long
LastRow = ws.Cells(ws.Rows.Count, LastRowColumn).End(xlUp).Row
Dim LastColumn As Long
LastColumn = ws.Cells(LastColumnRow, ws.Columns.Count) _
.End(xlToLeft).Column
Dim rng As Range
Set rng = ws.Range(ws.Cells(FirstRow, FirstColumn), _
ws.Cells(LastRow, LastColumn))
Dim Data1 As Variant: Data1 = rng.Value
Set ws = wb.Worksheets(ws2Name)
Dim Data2 As Variant: Data2 = ws.Range(rng.Address).Value
Dim Result() As Variant
Dim i As Long, j As Long, k As Long
For i = 1 To UBound(Data1)
For j = 1 To UBound(Data1, 2)
If Data1(i, j) <> Data2(i, j) Then GoSub writeResult
Next j
Next i
If k > 0 Then
transpose2D Result
On Error GoTo MissingResultSheet
Set ws = wb.Worksheets(wsResult)
On Error GoTo 0
ws.Range(ws.Range(ResultFirstCell), _
ws.Cells(ws.Rows.Count, ws.Columns.Count)).Clear
ws.Range(ResultFirstCell).Resize(k, UBound(Result, 2)).Value = Result
MsgBox "Found '" & k & "' difference(s) in range '" _
& rng.Address(False, False) & "'.", vbInformation
Else
MsgBox "Found no differences in range '" _
& rng.Address(False, False) & "'.", vbExclamation
End If
Exit Sub
writeResult:
k = k + 1
ReDim Preserve Result(1 To 4, 1 To k)
Result(1, k) = k
Result(2, k) = getAddress(i + FirstRow - 1, j + FirstColumn - 1)
Result(3, k) = Data1(i, j)
Result(4, k) = Data2(i, j)
Return
MissingResultSheet:
If Err.Number = 9 Then
wb.Worksheets.Add After:=wb.Sheets(wb.Sheets.Count)
With ActiveSheet
.Name = wsResult
If .Range(ResultFirstCell).Row > 1 Then
.Range(ResultFirstCell).Offset(-1) _
.Resize(, UBound(Headers) + 1).Value = Headers
End If
End With
Resume ' i.e. the code continues with Set ws = wb.Worksheets(wsResult)
Else
'?
Exit Sub
End If
End Sub
Function getAddress(aRow As Long, aColumn As Long) As String
getAddress = ActiveSheet.Cells(aRow, aColumn).Address(False, False)
End Function
Sub transpose2D(ByRef Data As Variant)
Dim i As Long, j As Long
Dim Result As Variant
ReDim Result(LBound(Data, 2) To UBound(Data, 2), _
LBound(Data) To UBound(Data))
For i = LBound(Data) To UBound(Data)
For j = LBound(Data, 2) To UBound(Data, 2)
Result(j, i) = Data(i, j)
Next j
Next i
Data = Result
End Sub
This solution for converting a column number to a string without using objects Function to convert column number to letter? could be used to write a descent getAddress function.