I have a list of duplicate values in column A that will be added as keys to a dictionary. Then for each row in column A there are other duplicates values from column 3 to .columns.count. I need to add them to the dictionary as multiples items of each key. At the end I should have two columns: the first listing all the keys and the second all the items of each keys.
Here my tentative. Could you help find out how to fix it?
Sheets("Sheet3").Select
With Sheets("Sheet3")
lc = .Cells(1, .Columns.Count).End(xlToLeft).Column
LR = .Range("A" & Sheets("Competitor").Rows.Count).End(xlUp).row
For thisRow = 2 To LR
For thiscol = 2 To lc
'Debug.Print dict.Keys(0)
If Not dict.Exists(.Cells(thisRow, 1).Value2) And .Cells(thisRow, thiscol).Value2 <> "" Then
dict.Add .Cells(thisRow, 1).Value2, (.Cells(thisRow, thiscol).Value2)
Else
If dict.Exists(.Cells(thisRow, 1).Value2) And .Cells(thisRow, thiscol).Value2 <> "" Then
dict.Item(.Cells(thisRow, 1).Value2) = .Cells(thisRow, thiscol).Value2
End If
End If
Next thiscol
Next thisRow
this uses a Dictionary of dictionaries to return unique items for uniqe keys
Option Explicit
Sub main()
Dim iKey As Long
Dim valsDict As Scripting.Dictionary
Set valsDict = CreateObject("Scripting.Dictionary")
Dim cell As Range, cell2 As Range
With ActiveWorkbook.Sheets("Competitor") ' change "Competitor" to you actual source sheet name
For Each cell In .Range("A2", .Cells(.Rows.Count, "A").End(xlUp))
If Not valsDict.Exists(cell.value) Then valsDict.Add cell.value, New Scripting.Dictionary
For Each cell2 In .Range(cell.Offset(, 1), .Cells(cell.Row, .Columns.Count).End(xlToLeft))
valsDict(cell.value)(cell2.value) = cell2.value
Next
Next
With .Range("AA1") ' change "AA1" with the cell address you want to start writing down data from
For iKey = 0 To valsDict.Count - 1
.Offset(iKey).value = valsDict.Keys(iKey)
.Offset(iKey, 1).Resize(, valsDict.Items(iKey).Count) = valsDict.Items(iKey).Items
Next
End With
End With
End Sub
You mentioned that you'd want the resulting list in two columns. The following code will create a unique list of values from Column A, along with their corresponding values. The unique values will be listed in one column, and the corresponding values will be concatenated in the next column. Note that I've assumed that the Sheet1 contains the data, and that the results are to be placed in Sheet2.
Option Explicit
Sub CreateUniqueList()
Dim oDic As Object
Dim aResults() As Variant
Dim arrColIndex As Long
Dim LastRow As Long
Dim LastCol As Long
Dim thisRow As Long
Dim thisCol As Long
Set oDic = CreateObject("Scripting.Dictionary")
oDic.CompareMode = 1 'case-insensitive
With ActiveWorkbook.Worksheets("Sheet1")
LastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
LastCol = .Cells(1, .Columns.Count).End(xlToLeft).Column
ReDim aResults(1 To 2, 1 To LastRow)
arrColIndex = 0
For thisRow = 2 To LastRow
If Len(.Cells(thisRow, "A").Value) > 0 Then
If Not oDic.Exists(.Cells(thisRow, "A").Value) Then
arrColIndex = arrColIndex + 1
aResults(1, arrColIndex) = .Cells(thisRow, "A").Value
For thisCol = 2 To LastCol
aResults(2, arrColIndex) = aResults(2, arrColIndex) & ", " & .Cells(thisRow, thisCol).Value
Next thisCol
aResults(2, arrColIndex) = Mid(aResults(2, arrColIndex), 3)
oDic.Add .Cells(thisRow, "A").Value, arrColIndex
Else
For thisCol = 2 To LastCol
aResults(2, oDic(.Cells(thisRow, "A").Value)) = aResults(2, oDic(.Cells(thisRow, "A").Value)) & ", " & .Cells(thisRow, thisCol).Value
Next thisCol
End If
End If
Next thisRow
End With
If arrColIndex > 0 Then
ReDim Preserve aResults(1 To 2, 1 To arrColIndex)
With ActiveWorkbook.Worksheets("Sheet2")
With .Range("A1")
.CurrentRegion.ClearContents
.Resize(UBound(aResults, 2), 2).Value = Application.Transpose(aResults)
End With
.Activate
End With
Else
MsgBox "No items found!", vbExclamation
End If
Set oDic = Nothing
End Sub
Data
Header1 Header2 Header3 Header4
x 1 2 3
y 4 5 6
z 7 8 9
x 10 20 30
y 40 50 60
z 70 80 90
Results
x 1, 2, 3, 10, 20, 30
y 4, 5, 6, 40, 50, 60
z 7, 8, 9, 70, 80, 90
Hope this helps!
Related
I have different test dates and times that can be up to about 100 tests each time point. I received the data that was only a single column that consists of thousands of rows, which should have been delivered in a matrix type grid.
I have only copied a sample, which has 6 time points and up to 4 tests each. I need Excel to "recognize" when there is only a date/time in a cell, then copy that cell to the next date/time to paste in a new sheet and column.
Eventually, I was hoping to also have the Title of the test separated from the results. However, if this is not plausible without knowing the name of every test, I can skip it. This is the data I start with:
Title
01/02/2010 0:03
Ounces: 10.87
Concentration: 6.89 (L)
Expiration Date: 11/2/2019 5:47:00
01/06/2011 2:06
Ounces: 18.09
Concentration: 10.7 (H)
Expiration Date: 11/2/2019 5:47:00
Other: Resampled
01/06/2011 2:06
Ounces: 12.87
Concentration: 10.9 (H)
Expiration Date: 11/2/2019 5:47:00
Other: 2nd Sample
09/15/2012 7:07
Ounces: 8.53
Concentration: 9.72
Expiration Date: 12/5/2019 4:45:00
05/02/2013 15:52
Ounces: 11.62
Concentration: 8.42
05/09/2017 1:45
Ounces: 9.34
Concentration: 8.98
I created the following Excel VBA, but am still new at programming, especially loops within loops, so I could not figure out how to create the offset that is dynamic enough to both select the right cells, but to copy them over to a new column. I also have redundancy within the code.
Sub Transpose()
Dim dDate As Date
Dim NumberofTasks As Long
Dim x As Long
sSheet = ActiveSheet.Name
Sheets.Add
dSheet = ActiveSheet.Name
With Worksheets("Sheet1")
' All Data is in Column A
NumberofTasks = .Cells(.Rows.Count, "A").End(xlUp).Row
For x = 1 To NumberofTasks
Sheets(sSheet).Activate
If IsDate(.Range("A" & x).Value) Then '<-- check if current cell at Column A is Date
Range(Cells(x, 1), Cells(x, 1).Offset(4, 0)).Select
Selection.Copy
Sheets(dSheet).Activate
Selection.PasteSpecial Paste:=xlAll, Operation:=xlNone, SkipBlanks:=False _
, Transpose:=True
ActiveCell.Offset(1, 0).Select
End If
Next x
End With
End Sub
This is what I hoped would happen (but on a much larger scale):
However, the offset places another date in another cell with the current code. Thank you for any help you can provide me.
There are many ways to skin a cat. Here is one way using arrays which is much much faster than looping through the range
Worksheet:
I am for the sake of coding, assuming that the data is in Sheet1 and looks like below
Logic:
Store the data from the worksheet in an array; Let's call it InputArray
Create an output array for storing data; Let's call it OutputArray
Loop through InputArray and find the date and then find the rest of the records. store in OutputArray
direct the output from OutputArray to the relevant worksheet.
Code:
Option Explicit
Sub Sample()
Dim InputArray As Variant
Dim ws As Worksheet
Dim i As Long
Dim recCount As Long
Dim lRow As Long
Dim OutputArray() As String
'~~> Set relevant input sheet
Set ws = Sheet1
With ws
'~~> Find Last Row in Col A
lRow = .Range("A" & .Rows.Count).End(xlUp).Row
'~~> Store col A in array
InputArray = .Range("A1:A" & lRow).Value
'~~> Find Total number of records
For i = LBound(InputArray) To UBound(InputArray)
If IsDate(InputArray(i, 1)) Then recCount = recCount + 1
Next i
'~~> Create an array for output
ReDim OutputArray(1 To 5, 1 To recCount + 1)
recCount = 2
'~~> Fill Col A of output array
OutputArray(1, 1) = "Title"
OutputArray(2, 1) = "Ounces"
OutputArray(3, 1) = "Concentration"
OutputArray(4, 1) = "Expiration Date"
OutputArray(5, 1) = "Other"
'~~> Loop through input array
For i = UBound(InputArray) To LBound(InputArray) Step -1
If IsDate(InputArray(i, 1)) Then '< Check if date
OutputArray(1, recCount) = InputArray(i, 1)
'~~> Check for Ounces and store in array
If i + 1 < UBound(InputArray) + 1 Then _
If UCase(Left(Trim(InputArray(i + 1, 1)), 2)) = "OU" _
Then OutputArray(2, recCount) = Trim(Replace(InputArray(i + 1, 1), "Ounces:", ""))
'~~> Check for Concentration and store in array
If i + 2 < UBound(InputArray) + 1 Then _
If UCase(Left(Trim(InputArray(i + 2, 1)), 2)) = "CO" _
Then OutputArray(3, recCount) = Trim(Replace(InputArray(i + 2, 1), "Concentration:", ""))
'~~> Check for Expiration Date and store in array
If i + 3 < UBound(InputArray) + 1 Then _
If UCase(Left(Trim(InputArray(i + 3, 1)), 2)) = "EX" _
Then OutputArray(4, recCount) = Trim(Replace(InputArray(i + 3, 1), "Expiration Date:", ""))
'~~> Check for Other and store in array
If i + 4 < UBound(InputArray) + 1 Then _
If UCase(Left(Trim(InputArray(i + 4, 1)), 2)) = "OT" _
Then OutputArray(5, recCount) = Trim(Replace(InputArray(i + 4, 1), "Other:", ""))
recCount = recCount + 1
End If
Next i
End With
'~~> Output it to relevant sheet
Sheet2.Range("A1").Resize(5, recCount - 1).Value = OutputArray
End Sub
Output:
I think here is better way to do it using Range.Find
Assuming the Data is in 1st Column of Sheet1 ie. Column A
In Demo the Expiration Date is not right, I have corrected that in the Code.
Try this code:
Sub TP()
Dim wk As Worksheet: Set wk = ThisWorkbook.Worksheets("Sheet1")
Dim lr As Long: lr = wk.Cells(wk.Rows.Count, "A").End(xlUp).row
Dim rng As Range
Dim i As Long
Dim j As Long
j = 4
For i = 3 To lr
Set rng = wk.Range(Cells(i, 1), Cells(i, 1).End(xlDown))
wk.Cells(2, j).Value = rng.Cells(1, 1).Value
Set fnd = rng.Find("Ounces")
If Not fnd Is Nothing Then wk.Cells(3, j).Value = Split(fnd.Value, ":")(1)
Set fnd = Nothing
Set fnd = rng.Find("Concentration")
If Not fnd Is Nothing Then wk.Cells(4, j).Value = Split(fnd.Value, ":")(1)
Set fnd = Nothing
Set fnd = rng.Find("Expiration")
If Not fnd Is Nothing Then wk.Cells(5, j).Value = Right(fnd.Value, Len(fnd.Value) - Len(Split(fnd.Value, ":")(0)) - 2)
Set fnd = Nothing
Set fnd = rng.Find("Other")
If Not fnd Is Nothing Then wk.Cells(6, j).Value = Split(fnd.Value, ":")(1)
Set fnd = Nothing
i = Cells(i, 1).End(xlDown).row + 1
j = j + 1
Next
End Sub
Demo:
May try something like this. Original code was modified and organized to complete the task intended. It takes cares if the other parameters of the test result are not organised in sequence as shown, blank row in between the parameters, no blank row between test results and or missing parameters. It only considers parameters found between rows of two test titles (date time). Takes only 0.5 seconds to process 200 test results from more than 1 K rows.
Option Explicit
Sub Transpose()
Dim dDate As Date
Dim NumberofTasks As Long
Dim x As Long, LastRow As Long, Xval As Variant
Dim srcWs As Worksheet, trgWs As Worksheet
Dim tm As Double
tm = Timer
Set srcWs = ThisWorkbook.ActiveSheet
Set trgWs = ThisWorkbook.Worksheets.Add
trgWs.Cells(1, 1).Value = "Title"
trgWs.Cells(2, 1).Value = "Ounces:"
trgWs.Cells(3, 1).Value = "Concentration:"
trgWs.Cells(4, 1).Value = "Expiration Date:"
trgWs.Cells(5, 1).Value = "Other:"
With srcWs
LastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
NumberofTasks = 0
x = 1
Do While x <= LastRow
Xval = .Cells(x, 1).Value
If IsDate(Xval) Then
NumberofTasks = NumberofTasks + 1
trgWs.Cells(1, NumberofTasks + 1).Value = .Range("A" & x).Value
ElseIf VarType(Xval) = vbString And NumberofTasks > 0 Then
Xval = Trim(LCase(Xval))
If InStr(1, Xval, "ounces:") > 0 Then
trgWs.Cells(2, NumberofTasks + 1).Value = Trim(Replace(Xval, "ounces:", ""))
ElseIf InStr(1, Xval, "concentration:") > 0 Then
trgWs.Cells(3, NumberofTasks + 1).Value = Trim(Replace(Xval, "concentration:", ""))
ElseIf InStr(1, Xval, "expiration date:") > 0 Then
trgWs.Cells(4, NumberofTasks + 1).Value = Trim(Replace(Xval, "expiration date:", ""))
ElseIf InStr(1, Xval, "other:") > 0 Then
trgWs.Cells(5, NumberofTasks + 1).Value = Trim(Replace(Xval, "other:", ""))
End If
End If
x = x + 1
Loop
End With
'Debug.Print "Seconds "; Timer - tm
End Sub
Tested to produce the result like
this
I have a list of projects in Excel. Each project has three rows (act, plan, fcst) and many columns (one column = one month).
What I would like to do is following:
A) for each unique value in column D (project #) add two rows | Completed
B) subtract plan - actual in one of new rows
C) subtract fcst - actual in second of new rows
A*) create two new rows and copy
data from columns A:AE for each unique value in column D (project #)
| Optional - I can handle option A), but A* would be a better one.
Does anyone know how to write a code to do points B, C, A*? I have no clue how to tackle that.
This is the final output that I would like to see (yellow and orange rows are new ones that I want macro to create for each unique project# in column D):
Text in AF is always either "Plan $000's" or "Actual $000's" or "Forecast $000's", for each project (i.e. each single project has these three rows; no less, no more).
Data is sorted per impact # (column D). Meaning that first three rows are related to project #123, next three are related to project #129, next three to project #761, etc.
We are allowed to play (sort, filter, etc.) with the data as long as we get the desired result. :-)
Below is the code I have right now... it is quite poor:
Sub CreateAndCompare()
Dim rng As Range
Dim cl As Range
Dim dic As Object
Dim ky As Variant
Set dic = CreateObject("Scripting.Dictionary")
With Sheets("Impact")
Set rng = .Range(.Range("D2"), .Range("D" & .Rows.Count).End(xlUp))
End With
For Each cl In rng
If Not dic.exists(cl.Value) Then
dic.Add cl.Value, cl.Value
End If
Next cl
For Each ky In dic.keys
lastrow = ActiveSheet.Range("d2").CurrentRegion.Rows.Count
Cells(lastrow + 1, 4).Value = dic(ky)
Cells(lastrow + 2, 4).Value = dic(ky)
Next ky
End Sub
thank you!
I think I have found the solution. :-)
I have created an extra column AG which concatenates Impact# & Purpose (columns D&AF).
However, it takes ~15 minutes to execute the code.
Is anyone able to suggest what should I change to make it faster?
Sub CreateAndCompare()
Dim rng As Range
Dim cl As Range
Dim dic As Object
Dim ky As Variant
Set dic = CreateObject("Scripting.Dictionary")
With Sheets("Impact")
Set rng = .Range(.Range("D2"), .Range("D" & .Rows.Count).End(xlUp))
End With
For Each cl In rng
If Not dic.exists(cl.Value) Then
dic.Add cl.Value, cl.Value
End If
Next cl
For Each ky In dic.keys
lastrow = ActiveSheet.Range("d2").CurrentRegion.Rows.Count
Cells(lastrow + 1, 4).Value = dic(ky)
Cells(lastrow + 1, 32).Value = "Act-Plan"
Cells(lastrow + 1, 33).Value = "Plan $000's"
For i = 2 To 43
mylookupvalue = Cells(lastrow + 1, 4) & "Actual $000's"
mylookupvalue_2 = Cells(lastrow + 1, 4) & Cells(lastrow + 1, 33)
myfirstcolumn = 33
mylastcolumn = 43
mycolumnIndex = i
myfirstrow = 2
mylastrow = lastrow
mytablearray = Worksheets("Impact").Range(Cells(myfirstrow, myfirstcolumn), Cells(mylastrow, mylastcolumn))
On Error Resume Next
value_1 = Application.WorksheetFunction.VLookup(mylookupvalue, mytablearray, mycolumnIndex, False)
value_2 = Application.WorksheetFunction.VLookup(mylookupvalue_2, mytablearray, mycolumnIndex, False)
Cells(lastrow + 1, i + 32).Value = value_1 - value_2
Cells(lastrow + 2, 4).Value = dic(ky)
Cells(lastrow + 2, 32).Value = "Act-Fcst"
Cells(lastrow + 2, 33).Value = "Forecast $000's"
mylookupvalue_3 = Cells(lastrow + 2, 4) & "Actual $000's"
mylookupvalue_4 = Cells(lastrow + 2, 4) & Cells(lastrow + 2, 33)
value_3 = Application.WorksheetFunction.VLookup(mylookupvalue_3, mytablearray, mycolumnIndex, False)
value_4 = Application.WorksheetFunction.VLookup(mylookupvalue_4, mytablearray, mycolumnIndex, False)
Cells(lastrow + 2, i + 32).Value = value_3 - value_4
Next i
Next ky
Worksheets("Impact").Range("AH2:BW10024").NumberFormat = "_(* #,##0_);_(* (#,##0);_(* ""-""??_);_(#_)"
End Sub
I suggest the following:
Loop through all data rows
Find the rows plan/actual/forecast for the current impact no
Then write the calculations to the end of the worksheet
So you would end up with something like that:
Option Explicit
Public Sub CreateAndCompare()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Impact")
'we assume here that the sheet is already sorted by column D "Impact #"
Dim LastDataRow As Long 'find last used row
LastDataRow = ws.Cells(ws.Rows.Count, "D").End(xlUp).Row
Dim LastDataColumn As Long 'find last used column
LastDataColumn = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
Dim NextEmptyRow As Long
NextEmptyRow = LastDataRow + 1
Dim iRow As Long, PlanRow As Long, ActualRow As Long, ForcastRow As Long
For iRow = 2 To LastDataRow 'loop through all data rows
Select Case ws.Cells(iRow, "AF").Value 'check which row type the current iRow is and remember
Case "Plan $000's": PlanRow = iRow
Case "Actual $000's": ActualRow = iRow
Case "Forecast $000's": ForcastRow = iRow
End Select
'detect change of impact no
If ws.Cells(iRow, "D").Value <> ws.Cells(iRow + 1, "D").Value Or iRow = LastDataRow Then
'check if plan/actual/forecast rows were found (if one is missing we cannot calculate
If PlanRow > 0 And ActualRow > 0 And ForcastRow > 0 Then
'copy column A-AE to next 2 empty rows
ws.Cells(NextEmptyRow, "A").Resize(RowSize:=2, ColumnSize:=31).Value = ws.Cells(iRow, "A").Resize(ColumnSize:=31).Value
'write purpose
ws.Cells(NextEmptyRow, "AF").Value = "Act - Plan"
ws.Cells(NextEmptyRow + 1, "AF").Value = "Act - Fcst"
'calculate
Dim iCol As Long
For iCol = 33 To LastDataColumn
ws.Cells(NextEmptyRow, iCol).Value = ws.Cells(ActualRow, iCol).Value - ws.Cells(PlanRow, iCol).Value
ws.Cells(NextEmptyRow + 1, iCol).Value = ws.Cells(ActualRow, iCol).Value - ws.Cells(ForcastRow, iCol).Value
Next iCol
NextEmptyRow = NextEmptyRow + 2 'initialize for next impact no
End If
PlanRow = 0: ActualRow = 0: ForcastRow = 0 'initialize for next impact no
End If
Next iRow
End Sub
I have 2 tables in Excel. The itemx, attributex in attrDICTIONARY needs to be updated based on Sheet 2.
I want to go through each itemx, attributex in Sheet 2
If it is not found in attrDICTIONARY, add a new row with the missing itemx, attributex in
Note: These columns are sorted in alphabetical order A-Z by itemx. There are also a large number of entries in Sheet 2 relative to attrDICTIONARY.
attrDICTIONARY contains:
column1 column2
item1 attribute1
item2 attribute2
item4 attribute4
Sheet 2 contains:
column1 column2
item1 attribute1
item2 attribute2
item3 attribute3
item4 attribute4
I have tried this:
Sub addAttributesToAttrDICTIONARY()
'
' addAttributesToAttrDICTIONARY Macro
'
Sheet2LastRow = Worksheets("Sheet2").Range("A" & Rows.Count).End(xlUp).Row
attrDictionaryLastRow = Worksheets("attrDICTIONARY").Range("C" & Rows.Count).End(xlUp).Row
Dim i As Integer
Dim j As Integer
j = 1
For i = 2 To Sheet2LastRow
While j <= attrDictionaryLastRow
incrementj:
j = j + 1
If (StrComp(Worksheets("Sheet2").Cells(i, 1).Value, Worksheets("attrDICTIONARY").Cells(j, 2).Value)) = 0 And (StrComp(Worksheets("Sheet2").Cells(i, 2).Value, Worksheets("attrDICTIONARY").Cells(j, 3).Value)) = 0 Then
GoTo Nexti
Else
Worksheets("attrDICTIONARY").Rows(j).Insert
Worksheets("attrDICTIONARY").Cells(j, 2).Value = Worksheets("Sheet2").Cells(i, 1).Value
Worksheets("attrDICTIONARY").Cells(j, 3).Value = Worksheets("Sheet2").Cells(i, 2).Value
attrDictionaryLastRow = attrDictionaryLastRow + 1
GoTo Nexti
End If
Wend
Nexti:
Next i
End Sub
Since the tables are sorted, I am just checking if they are the same, if not add a row above and add the appropriate values.
This code works up until about 4000 thousand items. At that point, it seems like the code stops checking for duplicates and just adds new rows for everything, pushing the original values down to the bottom and creating duplicates. I checked using a different coloured font for new items.
Any help would be appreciated. Thanks.
Give this a try:
Sub addAttributesToAttrDICTIONARY()
Dim wb As Workbook
Dim wsAttr As Worksheet
Dim wsData As Worksheet
Dim rAttr As Range
Dim aData As Variant
Dim aAdd() As Variant
Dim AddIndex As Long
Dim i As Long, j As Long
Set wb = ActiveWorkbook
Set wsAttr = wb.Sheets("attrDICTIONARY")
Set wsData = wb.Sheets("Sheet2")
Set rAttr = wsAttr.Range("B2", wsAttr.Cells(wsAttr.Rows.Count, "C").End(xlUp))
aData = wsData.Range("A2", wsData.Cells(wsData.Rows.Count, "A").End(xlUp)).Resize(, 2).Value
ReDim aAdd(1 To 65000, 1 To UBound(aData, 2))
For i = 1 To UBound(aData, 1)
If WorksheetFunction.CountIfs(rAttr.Columns(1), aData(i, 1), rAttr.Columns(2), aData(i, 2)) = 0 Then
AddIndex = AddIndex + 1
For j = 1 To UBound(aData, 2)
aAdd(AddIndex, j) = aData(i, j)
Next j
End If
Next i
If AddIndex > 0 Then
rAttr.Offset(rAttr.Rows.Count).Resize(AddIndex, UBound(aAdd, 2)).Value = aAdd
With wsAttr.Range("B2", wsAttr.Cells(wsAttr.Rows.Count, "C").End(xlUp))
.Sort .Resize(, 1), xlAscending, .Offset(, 1).Resize(, 1), , xlAscending, Header:=xlNo
End With
End If
End Sub
I am trying to get all row combinations of all columns (say 8 columns). The following vba macro can do that but I get an error that says data overload:
Option Explicit
Const sTitle As String = "shg Cartesian Product"
Sub CartesianProduct()
' shg 2012, 2013
' Choose one from col A, one from col B, ...
Dim rInp As Range
Dim avInp As Variant ' ragged input list
Dim nCol As Long ' # columns in list
Dim rOut As Range ' output range
Dim iCol As Long ' column index
Dim iRow As Long ' row index
Dim aiCum() As Long ' cum count of arrangements from right to left
Dim aiCnt() As Long ' count of items in each column
Dim iArr As Long ' arrangement number
Dim avOut As Variant ' output buffer
Application.ScreenUpdating = False
Set rInp = Range("rgnInp")
If VarType(rInp.Value) = vbEmpty Then
MsgBox Prompt:="No input!", _
Buttons:=vbOKOnly, _
Title:=sTitle
Exit Sub
End If
Set rInp = rInp.CurrentRegion
If rInp.Columns.Count < 2 Or rInp.Rows.Count < 2 Then
MsgBox Prompt:="Must have more than one row and more than one columns!", _
Buttons:=vbOKOnly, _
Title:=sTitle
Exit Sub
End If
With rInp
.Style = "Input"
avInp = .Value
nCol = .Columns.Count
Set rOut = .Resize(1).Offset(.Rows.Count + 1)
Range(rOut.Offset(-1, -1), Cells(Rows.Count, Columns.Count)).Clear
End With
ReDim aiCum(1 To nCol + 1)
ReDim aiCnt(1 To nCol)
aiCum(nCol + 1) = 1
For iCol = nCol To 1 Step -1
For iRow = 1 To UBound(avInp, 1)
If IsEmpty(avInp(iRow, iCol)) Then Exit For
aiCnt(iCol) = aiCnt(iCol) + 1
Next iRow
aiCum(iCol) = aiCnt(iCol) * aiCum(iCol + 1) <------ This is where it says error is
Next iCol
If aiCum(1) > Rows.Count - rOut.Row + 1 Then
MsgBox Prompt:=Format(aiCum(1), "#,##0") & _
" is too many rows!", _
Buttons:=vbOKOnly, Title:=sTitle
Exit Sub
End If
ReDim avOut(1 To aiCum(1), 1 To nCol)
For iArr = 1 To aiCum(1)
For iCol = 1 To nCol
avOut(iArr, iCol) = avInp((Int((iArr - 1) * aiCnt(iCol) / aiCum(iCol))) Mod aiCnt(iCol) + 1, iCol)
Next iCol
Next iArr
With rOut.Resize(aiCum(1), nCol)
.NumberFormat = "#"
.Value = avOut
.Style = "Code"
.Cells(1, 0).Value = 1
.Cells(2, 0).Value = 2
.Cells(1, 0).Resize(2).AutoFill .Columns(0)
End With
ActiveWindow.FreezePanes = False
rOut.EntireColumn.AutoFit
ActiveSheet.UsedRange
Beep
End Sub
Is there away to adjust for this? I also want it to not bring back the same values for a row. So lets say that two columns had the exact same data. If column A has lets say Ice cream, cake, and cookies and so does Column B, I don't want Row 1 to have cookies in column B if it is already picked in Column A.
I get output from a program imported to Excel in the following format:
Item 1
1 10
2 10
3 20
5 20
8 30
13 30
Item 2
1 40
2 40
3 50
5 50
8 60
13 60
Item 3
1 50
2 50
3 40
5 40
8 30
13 30
Now, I want to create a table where the values for each item is placed next to each other as below:
Item 1 Item 2 Item 3
1 10 40 50
2 10 40 50
3 20 50 40
5 20 50 40
8 30 60 30
13 30 60 30
I can think of ways to do this using formulas with a combination of INDIRECT other functions, but I can see right away that it will be a huge pain. Is there a clever way of doing this?
My approach would be something like this:
=VLOOKUP($A6;indirect("A"&(6+G$5*$X$4):"D"&(30+G$5*$X$4));4;FALSE)
where my first lookup table is from A6:D30, the second from A32:D56. X4 contains the value 26 which is the number of rows for each Item, and G5:AA5 is 0, 1, 2 ....
I would place this besides the Item 1 list and drag it sideways and downwards. I think the procedure should work, but I get syntax error.
I don't have much experience writing VBA, but I'm capable of reading and understanding it.
UPDATE:
At Siddharth's request:
Can you check out this.
It assumes a fixed format as it is shown in your example.
It can be made dynamic, but then you need to customize the code.
Option Explicit
Sub test()
Dim oCollection As Collection
Dim oDict As Variant
Dim oItem As Object
Dim iCnt As Integer
Dim iCnt_B As Integer
Dim iCnt_items As Integer
Dim iCnt_records As Integer
Dim iID As Integer
Dim iValue As Integer
Dim strKey As Variant
'Nr of items
iCnt_items = 3
'Records per item
iCnt_records = 6
'This dictionary will store the items
Set oCollection = New Collection
'Store dictionaries in collection
For iCnt = 0 To iCnt_items - 1
Set oDict = CreateObject("Scripting.Dictionary")
For iCnt_B = 1 To iCnt_records
iID = ThisWorkbook.Sheets(1).Cells((iCnt * (iCnt_records) + (iCnt + 1) + iCnt_B), 1).Value
Debug.Print iID
iValue = ThisWorkbook.Sheets(1).Cells((iCnt * (iCnt_records) + (iCnt + 1) + iCnt_B), 2).Value
Debug.Print iValue
oDict.Add iID, iValue
Next iCnt_B
oCollection.Add oDict, "item " & iCnt
Next iCnt
'Write collection to sheet
iCnt = 0
For Each oItem In oCollection
iCnt = iCnt + 1
ThisWorkbook.Sheets(2).Cells(1, 1 + iCnt).Value = "item " & iCnt
iCnt_B = 0
For Each strKey In oItem.keys
iCnt_B = iCnt_B + 1
ThisWorkbook.Sheets(2).Cells(1 + iCnt_B, 1).Value = strKey
ThisWorkbook.Sheets(2).Cells(1 + iCnt_B, 1 + iCnt).Value = oItem(strKey)
Next
Next oItem
End Sub
Edit: sorry for interrupting the conversation -> I didn't follow up the comment section while programming.
Sidenote:
If the ranges you work with are dynamic, I would go with a dictionary.
The reason why I'm saying this is because the dictionary object uses indexing on its records.
The key - pair structure being: ID, value
allows you to directly access the values corresponding the given ID.
In your example you are working with a clear ID - value structure.
Using numeric id's would actually be the fastest.
Since I already worked on this... Here is another way..
Assumptions:
Data starts at row 5 in Sheet1
Output will be generated in Sheet2
Code:
The below code uses Collections and Formulas to achieve what you want.
Sub Sample()
Dim wsInput As Worksheet, wsOutput As Worksheet
Dim ColItems As New Collection, ColSubItems As New Collection
Dim lRow As Long, i As Long, N As Long
Dim itm
Set wsInput = ThisWorkbook.Sheets("Sheet1")
Set wsOutput = ThisWorkbook.Sheets("Sheet2")
With wsInput
lRow = .Range("B" & .Rows.Count).End(xlUp).Row
.Columns(1).Insert
.Range("A5:A" & lRow).Formula = "=IF(ISERROR(SEARCH(""Item"",B5,1)),A4,B5)"
For i = 5 To lRow
On Error Resume Next
If InStr(1, .Range("B" & i).Value, "item", vbTextCompare) Then
ColItems.Add .Range("B" & i).Value, CStr(.Range("B" & i).Value)
Else
ColSubItems.Add .Range("B" & i).Value, CStr(.Range("B" & i).Value)
End If
On Error GoTo 0
Next i
End With
With wsOutput
.Cells.ClearContents
N = 2
'~~> Create Header in Row 1
For Each itm In ColItems
.Cells(1, N).Value = itm
N = N + 1
Next
N = 2
'~~> Create headers in Col 1
For Each itm In ColSubItems
.Cells(N, 1).Value = itm
N = N + 1
Next
lRow = .Range("A" & .Rows.Count).End(xlUp).Row
lcol = .Cells(1, .Columns.Count).End(xlToLeft).Column
j = 2
For i = 2 To lcol
.Range(.Cells(j, i), .Cells(lRow, i)).Formula = "=SUMIFS(" & _
wsInput.Name & _
"!C:C," & wsInput.Name & _
"!A:A," & .Name & _
"!$" & _
Split(.Cells(, i).Address, "$")(1) & _
"$1," & _
wsInput.Name & _
"!B:B," & _
.Name & _
"!A:A)"
Next i
.Rows("1:" & lRow).Value = .Rows("1:" & lRow).Value
End With
wsInput.Columns(1).Delete
End Sub
Screenshot:
This is what I have tried.
Sheet 1 contains the data. The result is generated in Sheet 2
Sub createTable()
Dim counter As Integer
Dim countRow As Integer
Dim flag As Boolean
Dim cellAddress As String
flag = True
countRow = 2
counter = 2
ThisWorkbook.Sheets("Sheet1").Activate
For Each cell In Range("a:a")
If counter = 2 Then
If InStr(1, cell.Value, "Item") Then
ThisWorkbook.Sheets("Sheet2").Activate
ActiveSheet.Cells(1, counter).Value = cell.Value
firstItem = cell.Value
counter = counter + 1
End If
Else
ThisWorkbook.Sheets("Sheet2").Activate
If InStr(1, cell.Value, "Item") Then
ThisWorkbook.Sheets("Sheet2").Activate
ActiveSheet.Cells(1, counter).Value = cell.Value
counter = counter + 1
flag = False
End If
If flag = True Then
Cells(cell.Row, cell.Column) = cell.Value
End If
End If
If cell.Value = vbNullString Then
Exit For
End If
Next cell
ThisWorkbook.Sheets("Sheet1").Activate
Application.CutCopyMode = False
Dim counteradd As Integer
counteradd = 2
For Each cell In Range("a:a")
v = cell.Value
If InStr(1, cell.Value, "Item") Then
If cell.Offset(1, 1).Select <> vbNullString Then
Range(Selection, Selection.End(xlDown)).Select
Selection.Copy
Sheets("Sheet2").Select
Cells(2, counteradd).Select
ActiveSheet.Paste
Application.CutCopyMode = False
counteradd = counteradd + 1
ThisWorkbook.Sheets("Sheet1").Activate
End If
End If
Next cell
End Sub