Capturing data between header rows based on a variable - vba

I'm having difficulty looping through rows of data between header rows to extract information and then sum the data from a few of the columns below the header row until it reaches the next header row. I am by no means a VBA expert vut Im trying to figure this out on my own and I've stripped down the code to just the basics trying to get this portion to work properly. I'm not sure if I'm taking the right approach to this, but I'm importing the data from the "Raw Data" worksheet into an array ("rdA", currently working fine), then trying to put the header data I need into one temporary array ("rdB", works for the first line, then gives an "Out of Range error) and information from the data rows below it into another temporary array ("rdC") so that I can try to sum data and add the sums to the first temporary array.
The header row always starts with [StartIspn] and I need to extract specific data from the header row (time stamp, user ID, and side). I then need to sum data from a few columns of the rows below, but only for the rows that contain "A13" in column E. The sample image below shows what the raw data looks like. The top gray header row is just in this example to define the columns of data between the headers. My thoughts are that this needs to be loops inside of loops to gather and sum the necessary data, but I'm currently getting stuck trying to get the data to go into the temp arrays. My end goal is to create an array that contains Wafer S/N (column B of rows between headers), Time Stamp, User ID, Wafer Side (all from each header row), and the sum of column F, sum of column H, Min of column I and Max of column J for all rows containing "A13" in column 6 between header rows.
If I can at least get some guidance as to whether or not the approach I'm using is wrong, and how to get past the out of range error when trying to add data to the temporary arrays, I'd be grateful.
Here's what I have so far:
' Define that arrays start with index 1 instead of 0
Option Base 1
' Define that variables must be defined manually and will never be defined automatically
Option Explicit
Sub Create_Report()
' Define variable names and types
Dim chkAnn As String ' Check column 5 for inspection type (A13)
Dim chkHdr As String ' Check column 2 for StartIspn or S/N
Dim fmTot As String ' Sum the total FM area per inspection
Dim fmNum As Long ' Sum the total number of FM particles per inspection
Dim fmMin As Long ' Find the min FM particle size per inspection
Dim fmMax As Long ' Find the max FM particle size per inspection
Dim h As Long ' Row count for FM data
Dim i As Long ' Row count of number of rows being processed
Dim idCol As String ' Time stamp from raw data header line
Dim idPos As Long ' Position of time stamp in raw data header cell
Dim idVal As String ' Time stamp from ecah inspection
Dim j As Long ' Row count for report data array
Dim k As Long ' Row count for debug print
Dim lRow As Long ' Count of number of rows in Raw Data
Dim m As Long ' Row count for debug print
Dim tsCol As String ' Time stamp from raw data header line
Dim tsPos As Long ' Position of time stamp in raw data header cell
Dim tsVal As String ' Time stamp from ecah inspection
Dim rdA() As Variant ' Array of imported Raw Data for parsing
Dim rdB() As Variant ' Array of processed data for report output
Dim rdC() As Variant ' Temp array of FM totals
Dim wfrSN As String ' Wafer serial number from line below header row
Dim wsCol As String ' Time stamp from raw data header line
Dim wsPos As Long ' Position of time stamp in raw data header cell
Dim wsVal As String ' Time stamp from ecah inspection
' Clear all arrays and variables in case report is run again
Erase rdA
ReDim rdA(1, 1)
Erase rdB
ReDim rdB(1, 1)
h = 0
i = 0
j = 0
k = 0
' Find number of populated rows in Raw Data
lRow = Worksheets("Raw Data").Cells(Rows.Count, "B").End(xlUp).Row
' Create array of data from "Raw Data" worksheet
rdA = Worksheets("Raw Data").Range("A1:Q1").Resize(lRow, 17).Value2
' PER INSPECTION GROUP
' Check each line of raw data and extract required info from header row
j = 1
For i = LBound(rdA, 1) To UBound(rdA, 1)
chkHdr = rdA(i, 2)
chkAnn = rdA(i, 5)
Const Hdr = "[StartIspn]"
' Check row for [StartIspn] in rdA Col 2
If InStr(1, chkHdr, Hdr, vbBinaryCompare) > 0 Then
' Collect Wafer Serial Number from next row and add to report array
wfrSN = rdA(i + 1, 2)
rdB(j, 1) = wfrSN
' Collect Time Stamp of inspections and add to report array
tsCol = rdA(i, 3)
tsPos = InStrRev(tsCol, "=")
tsVal = Mid$(tsCol, tsPos + 1)
rdB(j, 2) = tsVal
' Collect User ID and add to report array
idCol = rdA(i, 4)
idPos = InStrRev(idCol, "=")
idVal = Mid$(idCol, idPos + 1)
rdB(j, 3) = idVal
' Collect Wafer Side and add to report array
wsCol = rdA(i, 6)
wsPos = InStrRev(wsCol, "=")
wsVal = Mid$(wsCol, wsPos + 1)
If wsVal = "T" Then
wsVal = "Front"
ElseIf wsVal = "B" Then
wsVal = "Back"
End If
rdB(j, 4) = wsVal
' Resize the report array for the next data set
If j > 0 Then
ReDim Preserve rdB(j - 1)
End If
' Advance to next line in report array (rdB)
j = j + 1
Else
For h = LBound(rdA, 1) To UBound(rdA, 1)
chkAnn = rdA(h, 5)
Const Ann = "A13"
If InStr(1, chkAnn, Ann, vbBinaryCompare) > 0 Then
'Collect Wafer Serial Number
wfrSN = rdA(i, 2)
rdC(h, 1) = wfrSN
' Collect FM Total
fmTot = rdA(i, 6)
rdC(h, 2) = fmTot
' Collect # of FM Particles
fmNum = rdA(i, 8)
rdC(h, 3) = fmNum
' Collect Min Particle Size
fmMin = rdA(i, 9)
rdC(h, 4) = fmMin
' Collect Max Particle Size
fmMax = rdA(i, 10)
rdC(h, 5) = fmMax
' Advance to next line in temp array (rdC)
h = h + 1
End If
Next h
Next i
For k = LBound(rdB, 1) To UBound(rdB, 1)
Debug.Print rdB(k, 1) & ", " & _
rdB(k, 2) & ", " & _
rdB(k, 3) & ", " & _
rdB(k, 4)
Next k
For m = LBound(rdC, 1) To UBound(rdC, 1)
Debug.Print rdC(m, 1) & ", " & _
rdC(m, 2) & ", " & _
rdC(m, 3) & ", " & _
rdC(m, 4) & ", " & _
rdC(m, 5)
Next m
End Sub
Updated and working Code:
Sub Create_Report()
Dim vDB, vResult(), vSum1(), vSum2(), vMin(), vMax()
Dim Ws As Worksheet, wsResult As Worksheet
Dim s As String, i As Long, n As Long, r As Long
Dim k As Integer
Const Hdr = "[StartIspn]"
Const Ann = "A13"
Set Ws = Sheets("Raw Data")
Set wsResult = Sheets("AOI Inspection Summary")
vDB = Ws.Range("a1").CurrentRegion
r = UBound(vDB, 1)
For i = 1 To r
If InStr(vDB(i, 2), Hdr) Then
n = n + 1
ReDim Preserve vResult(1 To 9, 1 To n)
vResult(1, n) = n
vResult(2, n) = vDB(i + 1, 2)
vResult(3, n) = Replace(vDB(i, 3), "Time=", "")
vResult(4, n) = Replace(vDB(i, 4), "User=", "")
s = Replace(vDB(i, 6), "Side=", "")
If s = "T" Then
vResult(5, n) = "Front"
Else
vResult(5, n) = "Back"
End If
If k > 0 Then
vResult(6, n - 1) = WorksheetFunction.Sum(vSum1)
vResult(7, n - 1) = WorksheetFunction.Sum(vSum2)
vResult(8, n - 1) = WorksheetFunction.Min(vMin)
vResult(9, n - 1) = WorksheetFunction.Max(vMax)
k = 0
End If
Else
If InStr(vDB(i, 5), Ann) Then
k = k + 1
ReDim Preserve vSum1(1 To k)
ReDim Preserve vSum2(1 To k)
ReDim Preserve vMin(1 To k)
ReDim Preserve vMax(1 To k)
vSum1(k) = vDB(i, 6)
vSum2(k) = vDB(i, 8)
vMin(k) = vDB(i, 9)
vMax(k) = vDB(i, 10)
End If
End If
Next i
vResult(6, n) = WorksheetFunction.Sum(vSum1)
vResult(7, n) = WorksheetFunction.Sum(vSum2)
vResult(8, n) = WorksheetFunction.Min(vMin)
vResult(9, n) = WorksheetFunction.Max(vMax)
With wsResult 'array Result write on sheet
.Range("b21").CurrentRegion.Offset(2).ClearContents
.Range("b23").Resize(n, 9) = WorksheetFunction.Transpose(vResult)
End With
End Sub

Try this.
Sub test()
Dim vDB, vResult(), vSum(), vMin(), vMax()
Dim Ws As Worksheet, wsResult As Worksheet
Dim s As String, i As Long, n As Long, r As Long
Dim k As Integer
Const Hdr = "[StartIspn]"
Set Ws = Sheets("Raw Data")
Set wsResult = Sheets("AOI Inspection Summary")
vDB = Ws.Range("a1").CurrentRegion
r = UBound(vDB, 1)
For i = 2 To r '<~~ if your Raw data row 1 data is Row#, Watar S/n.... i start 2 else 1
If InStr(vDB(i, 2), Hdr) Then
n = n + 1
ReDim Preserve vResult(1 To 9, 1 To n)
vResult(1, n) = n
vResult(2, n) = vDB(i + 1, 2)
vResult(3, n) = Replace(vDB(i, 3), "Time=", "") 'time
vResult(4, n) = Replace(vDB(i, 4), "User=", "") 'Positon
s = Replace(vDB(i, 6), "Sided=", "")
If s = "T" Then
vResult(5, n) = "Front"
Else
vResult(5, n) = "Back"
End If
If k > 0 Then
vResult(6, n - 1) = WorksheetFunction.Sum(vSum)
vResult(7, n - 1) = 37 '<~~ what mean # of particle
vResult(8, n - 1) = WorksheetFunction.Min(vMin)
vResult(9, n - 1) = WorksheetFunction.Max(vMax)
k = 0
End If
Else
k = k + 1
ReDim Preserve vSum(1 To k)
ReDim Preserve vMin(1 To k)
ReDim Preserve vMax(1 To k)
vSum(k) = vDB(i, 6)
vMin(k) = vDB(i, 9)
vMax(k) = vDB(i, 10)
End If
Next i
vResult(6, n) = WorksheetFunction.Sum(vSum)
vResult(7, n) = 37 '<~~ what mean # of particle
vResult(8, n) = WorksheetFunction.Min(vMin)
vResult(9, n) = WorksheetFunction.Max(vMax)
With wsResult 'array Result write on sheet
.Range("b21").CurrentRegion.Offset(2).ClearContents
.Range("b23").Resize(n, 9) = WorksheetFunction.Transpose(vResult)
End With
End Sub

Related

Manipulating Collections and Arrays in Excel VBA to accommodate missing values and error handling

I did not know how to explain the question so I will attach images for explaining my situation. Here is the view of my Excel Sheet:
My Excel Sheet
The highlighted cells contain multiple values called ID's and are associated with respective Versions in the columns beside them. I use the following macro (details with great explanation here) to split these values into multiple rows in the same sheet.
Option Explicit
Private Const ID_IDX As Long = 0
Private Const VER_IDX As Long = 1
Private Const RNG_IDX As Long = 2
Private Sub RunMe()
Dim data As Variant, cols As Variant, items As Variant
Dim r As Long, c As Long, i As Long, n As Long
Dim ids() As String, vers() As String
Dim addItems As Collection, concatItems As Collection
Dim dataRng As Range, rng As Range
Dim writeID() As Variant, writeVer() As Variant, writeConcat() As Variant
Dim dataStartRow As Long
On Error Resume Next
'Define the range we're interested in and read into an array.
With Sheet1 'adjust for your worksheet object
Set dataRng = Application.InputBox(prompt:="Select the Range of cells:", Type:=8)
End With
data = dataRng.Value2
dataStartRow = 2
'Find the two target columns
cols = AcquireIdAndVerCol(data, 3, 8)
If IsEmpty(cols) Then
MsgBox "Unable to find Id and Ver columns."
Exit Sub
End If
With dataRng
'Add a column next to the version number column.
.Columns(cols(VER_IDX)).Offset(, 1).Insert Shift:=xlShiftToRight, CopyOrigin:=xlFormatFromLeftOrAbove
'Add a column to our range.
'This is to cover the case that the rightmost column is the version number column.
Set dataRng = .Resize(, .Columns.Count + 1)
End With
'Find the rows that need to be split and concatenate the target strings.
Set addItems = New Collection
Set concatItems = New Collection
For r = dataStartRow To UBound(data, 1)
ids = Split(data(r, cols(ID_IDX)), vbLf)
vers = Split(data(r, cols(VER_IDX)), vbLf)
n = IIf(UBound(ids) >= UBound(vers), UBound(ids), UBound(vers))
If n = 0 Then 'it's just one line of text.
'Add concatenated text to list.
concatItems.Add data(r, cols(ID_IDX)) & " " & data(r, cols(VER_IDX))
ElseIf n > 0 Then 'it's multiple lines of text.
'Transpose the id array.
ReDim writeID(1 To UBound(ids) + 1, 1 To 1)
For i = 0 To UBound(ids)
writeID(i + 1, 1) = ids(i)
Next
'Transpose the version array.
ReDim writeVer(1 To UBound(vers) + 1, 1 To 1)
For i = 0 To UBound(ids)
writeVer(i + 1, 1) = vers(i)
Next
'Add concatenated text to list.
For i = 0 To n
concatItems.Add (IIf(UBound(ids) <= n And UBound(vers) <= n, ids(i) & " " & vers(i), Empty))
Next
'Add the range to be split to the collection.
addItems.Add Array(writeID, writeVer, dataRng.Rows(r + 1).Resize(n))
Else 'it's an empty cell
'Add empty item to concatenated list in order to keep alignment.
concatItems.Add Empty
End If
Next
Application.ScreenUpdating = False
'Split the ranges in the list.
If addItems.Count > 0 Then
For Each items In addItems
'Add the rows.
With items(RNG_IDX)
.Insert Shift:=xlDown, CopyOrigin:=xlFormatFromLeftOrAbove
Set rng = .Offset(-.Rows.Count - 1).Resize(.Rows.Count + 1)
'Note: format your rng Range obect as desired here.
End With
'Write the id and version values.
rng.Columns(cols(ID_IDX)).Value = items(ID_IDX)
rng.Columns(cols(VER_IDX)).Value = items(VER_IDX)
Next
End If
'Write the concatenated values.
If concatItems.Count > 0 Then
ReDim writeConcat(1 To concatItems.Count + dataStartRow - 1, 1 To 1)
'Header to array.
writeConcat(1, 1) = "Concat values"
'Values from the collection to array.
i = dataStartRow
For Each items In concatItems
writeConcat(i, 1) = items
i = i + 1
Next
'Output array to range.
With dataRng.Columns(cols(VER_IDX) + 1)
.Value = writeConcat
.AutoFit
End With
End If
Application.ScreenUpdating = True
End Sub
Private Function AcquireIdAndVerCol(data As Variant, minCol As Long, maxCol As Long) As Variant
Dim result(1) As Long
Dim r As Long, c As Long, i As Long
Dim items() As String
'Check we're not operating outside bounds of data array.
If minCol < LBound(data, 2) Then minCol = LBound(data, 2)
If minCol > UBound(data, 2) Then minCol = UBound(data, 2)
If maxCol < LBound(data, 2) Then maxCol = LBound(data, 2)
If maxCol > UBound(data, 2) Then maxCol = UBound(data, 2)
'Loop through data to find the two columns.
'Once found, leave the function.
For r = 1 To UBound(data, 1)
For c = minCol To maxCol
items = Split(data(r, c), vbLf)
For i = 0 To UBound(items)
If result(ID_IDX) = 0 Then
If IsDocId(items(i)) Then
result(ID_IDX) = c
If result(VER_IDX) = 0 Then
Exit For
Else
AcquireIdAndVerCol = result
Exit Function
End If
End If
End If
If result(VER_IDX) = 0 Then
If IsDocVer(items(i)) Then
result(VER_IDX) = c
If result(ID_IDX) = 0 Then
Exit For
Else
AcquireIdAndVerCol = result
Exit Function
End If
End If
End If
Next
Next
Next
End Function
Private Function IsDocId(val As String) As Boolean
Dim n As Long
n = TryClng(val)
IsDocId = (n > 9999 And n <= 999999999)
End Function
Private Function IsDocVer(val As String) As Boolean
Dim n As Long, m As Long
Dim items() As String
items = Split(val, ".")
If UBound(items) <> 1 Then Exit Function
n = TryClng(items(0))
m = TryClng(items(1))
IsDocVer = (n > 0 And n <= 99) And (m >= 0 And m <= 9)
End Function
'-------------------------------------------------------------------
'Converts a variant to a Long or returns a fail value as a Long
'if the conversion failed.
'-------------------------------------------------------------------
Private Function TryClng(expr As Variant, Optional fail As Long = -1) As Long
Dim n As Long
n = fail
On Error Resume Next
n = CLng(expr)
On Error GoTo 0
TryClng = n
End Function
It gives the following output with an addition column named, Concat Values, which contains combined values of Id's and corresponding Versions:
Output
Problem:
It works flawlessly if all the ID's have corresponding Versions specified in the sheet separately as I mentioned above. However in cases, where there is only one Version number, and it's bound to 4 or more Id's, i.e. Same Version number is applicable for all the ID's, like such:
The output in the column Concat Values gets disoriented because we are using an array to output the Concat Values and the array is not accommodating the missing Versions for corresponding Id's. It looks like this:
Dislocated row values
I am trying to learn and figure out a way to update the collection and the array with new Concat Values before Outputting it to the column, so that each Concat Value gets placed in their corresponding ID and Version location. I hope that it makes sense. Please let me know for more clarification.
EDIT:
I will try and list all the possible Cases and Expected Output, including the worst case scenarios:
Here is the link to my excel sheet.
Usual Scenarios
Number of Id's = Number of Versions (Works perfectly, Concat Values get aligned in corresponding rows in the columns)
Multiple Id's - Single Version (In such cases, the Version # applicable to all the ID's is same i.e. one Version should be applied to all the ID's.)
Issue:
The Macro does the task of splitting the columns into rows, except the part where Concat values get misaligned.
Worst Case Scenarios
Multiple Id's - Multiple Versions, but less than total #ID's (In such cases, Versions should align to the topmost ID's and fill the ID's below with blanks)
Issue:
The Macro does the task of splitting the columns into rows, except the part where Concat values get misaligned.
Here 4 ID's have been given only 3 Versions, so Top 3 ID's are assigned 3 Versions and the 4th ID has no Version linked to it.
Similarly,
Here 4 ID's have been given only 2 Versions, so Top 2 ID's are assigned 2 Versions and the 3rd and 4th ID's have no Version linked to them.
Multiple Id's - No Version (In such cases, columns should split into rows based on #ID's and corresponding Version rows should be filled with blanks)
Issue:
The Macro does the task of splitting the columns into rows, except the part where Concat values get misaligned.
The complexity of the solution will depend on the complexity and variety of 'special cases'. Given your scenarios, it seems as if you could just take the last of the given versions and, for any versions missing below that line, just use that last used version.
When I gave my first answer, I anticipated this kind of issue, so changes to the code are trivial.
Firstly add an additional declaration in the RunMe Sub:
Dim curVer As String
and then you just need to adjust the ElseIf n > 0 case. Replace the code with this:
ElseIf n > 0 Then 'it's multiple lines of text.
'Resize the output arrays to max ('n')
ReDim writeID(1 To n + 1, 1 To 1)
ReDim writeVer(1 To n + 1, 1 To 1)
'Loop through the arrays to align id and versions.
For i = 0 To n
If i <= UBound(ids) Then
writeID(i + 1, 1) = ids(i)
End If
If i <= UBound(vers) Then
curVer = vers(i)
End If
writeVer(i + 1, 1) = curVer
Next
'Add concatenated text to list.
For i = 0 To n
concatItems.Add writeID(i + 1, 1) & " " & writeVer(i + 1, 1)
Next
'Add the range to be split to the collection.
addItems.Add Array(writeID, writeVer, dataRng.Rows(r + 1).Resize(n))
Too much code for me to read but I came up with my solution if I understood you problem correctly.
I guess it could be a good solution if you modify it. With my code it will be easier to produce a new table instead of adding rows I guess. Then you could just add the formatting which should be very easy.
Sub Test()
Dim xRange As Range
Dim xArrRange() As Variant
Dim xNewArrRange() As Variant
Dim xNewArrRangeResize() As Variant
Dim xNumberColumns As Long
Dim i As Long
Dim j As Long
Dim k As Long
Dim l As Long
Dim ii As Long
Dim jj As Long
Set xRange = Range("A2:C5")
xNumberColumns = 3
xArrRange = xRange.Value2
ReDim xNewArrRange(xRange.Rows.Count + 10, xNumberColumns) ' "xNumberColumns - 1" to have the number of columns
' "xNumberColumns" is one more
For i = LBound(xArrRange, 1) To UBound(xArrRange, 1)
Dim xTempArrVer As Variant
Dim xTempArrID As Variant
xTempArrVer = Split(xArrRange(i, 3), vbLf)
If UBound(xTempArrVer) = -1 Then ' If there are no version, initialize it with ""
ReDim xTempArrVer(0)
xTempArrVer(0) = ""
End If
xTempArrID = Split(xArrRange(i, 2), vbLf)
For j = LBound(xTempArrID, 1) To UBound(xTempArrID, 1)
If j > UBound(xTempArrVer, 1) Then
l = UBound(xTempArrVer, 1)
Else
l = j
End If
xNewArrRange(k, 0) = xArrRange(i, 1)
xNewArrRange(k, 1) = xTempArrID(j)
xNewArrRange(k, 2) = xTempArrVer(l)
If xTempArrVer(l) <> "" Then
xNewArrRange(k, 3) = xTempArrID(j) & " " & xTempArrVer(l)
Else
xNewArrRange(k, 3) = xTempArrID(j)
End If
k = k + 1
If k + 1 > UBound(xNewArrRange, 1) Then
ReDim Preserve xNewArrRange(UBound(xNewArrRange, 1) + 30, xNumberColumns)
End If
Next j
Next i
ReDim xNewArrRangeResize(k - 1, xNumberColumns) ' "xNumberColumns - 1" to have the number of columns
' "xNumberColumns" is one more
For ii = LBound(xNewArrRangeResize, 1) To UBound(xNewArrRangeResize, 1)
For jj = LBound(xNewArrRangeResize, 2) To UBound(xNewArrRangeResize, 2)
xNewArrRangeResize(ii, jj) = xNewArrRange(ii, jj)
Next jj
Next ii
Range(Cells(2, 6), Cells(UBound(xNewArrRangeResize, 1) + 1, 6 + UBound(xNewArrRangeResize, 2))).Value2 = xNewArrRangeResize
Debug.Print "Finish"
End Sub
This code produces this:
If your code produces good number of rows for each id etc, the most lazy solution would be just to populate columns of your table with part of my array which is produced at the end.
Edit:
I see there is something missing but that is because I calculated wrongly that Range.
Range(Cells(2, 6), Cells(UBound(xNewArrRangeResize, 1) + 1, 6 + UBound(xNewArrRangeResize, 2))).Value2 = xNewArrRangeResize

VBA: Generating numbers for unique values of range and returning them to the range (simulating dice rolls)

I am trying to implement a random number generation in an Excel sheet. The process is such:
There are seven cells, each containing the number and type of dice to be rolled in standard notation (XdY+Z, where X is the number of Y-sided dice to roll, with Z being the bonus/penalty)
The numbers are tallied up into unique groups by roll types
The numbers are generated for each group (I have this step working, so this isn't the problem).
One extra roll is made for each group
The lowest number is dropped
The numbers are assigned to an output range, in order, so they match their dice rows.
I know I can extract the unique values from my input using a collection. I also already have a function which interprets the dice type and makes the roll. I am stumped though about being able to tally up the unique values, roll that many times + 1, drop lowest and then return them to the correct rows. Especially since I don't want to sort the results.
I would appreciate any help or any direction in which you could point me.
Example:
Input:
1d6
1d6
1d8
1d10
1d4
1d6
1d4
Divide into buckets: 3 x 1d6; 1 x 1d8; 1 x 1d10; 2 x 1d4
Roll dice, with an extra roll for each bucket:
4 x 1d6 - 4, 4, 5, 2
2 x 1d8 - 8, 7
2 x 1d10 - 1, 3
3 x 1d4 - 1, 1, 4
Drop lowest value, leaving the following numbers:
1d6: 4, 4, 5
1d8: 8
1d10: 3
1d4: 1, 4
Assign them in order:
1d6 - 4
1d6 - 4
1d8 - 8
1d10 - 3
1d4 - 1
1d6 - 5
1d4 - 4
This is the original function, which simply goes down the list, generates the roll (through a RollDice function that performs the roll), and places it in the correct output cell:
Sub GenerateOld()
For i = 1 To 7
Range("Dice_Output").Cells(i).Value = _
RollDice(Range("Dice_Input").Cells(i).Value)
Next i
End Sub
This is my attempt at the new version of this code. Commented out are the sections I can't figure out:
Sub GenerateNew()
Dim diceDictionary
Set diceDictionary = CreateObject("Scripting.Dictionary")
For Each Cell In Range("Char_Characteristics_Dice").Cells
If diceDictionary.Exists(Cell.Value) Then
diceDictionary(Cell.Value) = diceDictionary(Cell.Value) + 1
Else
diceDictionary.Add Cell.Value, 1
End If
Next Cell
For Each diceType In diceDictionary
' RollDice(diceType)
' Roll X drop lowest
Next cont
' Place back into Dice_Output range in order
End Sub
Not sure if this is still needed, but I used a set of arrays to tackle this problem. Here's a summary of how I approached it:
Get the values from the range in Excel, pass them to the first array
Set up the number of times the dice needed to be rolled
Pass the first array to a 2D array and populate it with info to finish
Use a temporary array to get values from the rolls and then paste back into the Excel sheet
Sub roll()
Dim i As Long
Dim j As Long
Dim k As Long
Dim lr As Long
Dim upperbound As Long
Dim lowerbound As Long
Dim frequency As String
Dim rolls As String
Dim rng As Range
Dim arr1D() As String
Dim arr2D() As String
Dim rollresult As Integer
Dim arr_min As Variant
Dim FirstCheck As Boolean
Dim targetdi As Variant
'Set the area with values for the dice roll simulation
lr = Cells(Rows.Count, "A").End(xlUp).Row
'Clear the result area for roll results
Range(Cells(2, "B"), Cells(lr, "B")).ClearContents
Set rng = Range(Cells(2, "A"), Cells(lr, "A"))
'Collect unique values from the range
For Each cell In rng
If (cell <> "") And (InStr(frequency, cell) = 0) Then
frequency = frequency & cell & "|"
End If
Next cell
If Len(frequency) > 0 Then frequency = Left(frequency, Len(frequency) - 1)
arr1D = Split(frequency, "|")
'Set up the 2D array with a space for the number of rolls
ReDim arr2D(LBound(arr1D) To UBound(arr1D), LBound(arr1D) To 3)
'Copy contents from first (1D) array into the second (2D) array
For i = LBound(arr1D) To UBound(arr1D)
arr2D(i, 0) = arr1D(i)
arr2D(i, 1) = Application.WorksheetFunction.CountIf(Range(Cells(2, "A"), Cells(lr, "A")), "=" & arr2D(i, 0)) + 1
arr2D(i, 2) = Right(arr2D(i, 0), Len(arr2D(i, 0)) - InStr(1, arr2D(i, 0), "d"))
'Keep rollin rollin rollin WHAT Keep rollin rollin rollin
For j = 1 To (arr2D(i, 1))
If ((arr2D(i, 2)) <> "") Then
rollresult = Int((Int((arr2D(i, 2) + 1)) - 1 + 1) * Rnd + 1)
rolls = rolls & rollresult & "|"
End If
Next j
rolls = Left(rolls, Len(rolls) - 1)
arr2D(i, 3) = rolls
rolls = ""
Next i
For i = LBound(arr2D) To UBound(arr2D)
temparray = Split(arr2D(i, 3), "|")
arr_min = temparray(LBound(temparray))
For j = LBound(temparray) To UBound(temparray) 'LBound(temparray) To UBound(temparray) - 1
If temparray(j) < arr_min Then
arr_min = temparray(j)
End If
Next j
'Remove the lowest value, but preserve the order
For j = LBound(temparray) To UBound(temparray)
If temparray(j) = arr_min And FirstCheck = False Then
temparray(j) = ""
FirstCheck = True
End If
Next j
'Place the results back in the sheet
For j = LBound(temparray) To UBound(temparray)
If temparray(j) <> "" Then
targetdi = arr2D(i, 0)
For k = 2 To lr
If Cells(k, "A").Value = targetdi And Cells(k, "B").Value = "" Then
Cells(k, "B").Value = temparray(j)
End If
Next k
End If
Next j
Next i
End Sub

Excel vba How to get combination of the numbers output into excel rows?

I need to find out a way to output the result of all combinations of the numbers into rows ( best if could be in a single row)
I have 8 digits {1,2,3,4,5,6,7,8} the typical output for the combination is i;j (i, j are numbers from the set and i< j) if pick up two. To generate result is simple:
Dim Myarray_2 As String
Dim sht as Worksheet
set sht = Sheet1
Myarray_2 = "" ' pick up 2 out of 8
For j = 2 To 8
For i = 1 To j - 1
sht.Cells(i + 1, j + 1) = Str(MyArray(i)) + ";" + Str(MyArray(j))
Myarray_2 = Myarray_2 + Str(MyArray(i)) + ";" + Str(MyArray(j)) + "|"
Next i
Next j
This is an example for pick up 2, I already have it output to rows of a worksheet.
I also have solution for picking up 3, now my questions is for the rest of the cases, how to get the out put?
Here is the solution for picking up 3:
Dim Myarray_3 As String
Myarray_3 = "" ' 3 out of 8
k = 3
Do While k >= 3 And k <= 8
'inner loop through i j
For j = 2 To k - 1
For i = 1 To j - 1
sht.Cells(i + 11, j - 1 + m) = Str(MyArray(i)) + ";" + Str(MyArray(j)) + ";" + Str(MyArray(k))
Myarray_3 = Myarray_3 + Str(MyArray(i)) + ";" + Str(MyArray(j)) + ";" + Str(MyArray(k)) + "|"
Next i
Next j
k = k + 1
m = m + 7
Loop
By the way the MyArray(i) is initialized as Myarray(i) = i
I found some code that I got from another good programmer, I changed the code to fit to your problem. If you have N as the number of members of your set/array, then you will have (2^N)-1 combinations, however you can filter them using your own conditions. Note that in your problem, the order of the members would be important when it comes to filtration using your condition.
The code will first generate ALL OF THE COMBINATIONS and then apply the conditions. Array Result would be the main output so its size will be always (2^N)-1. Array Result-filtered will be what you want.
Note that if you have the numbers sorted from left to right, arrays Result and Result_filtered will be the same.
You can print the out put with any format you like into any sheet.
This method uses bitwise calculation to get the combinations:
if N=2, then number of comnibations would be (2^2)-1=3
we always exclude 0 'in binary of course
{A,B} -> { [00],[01],[10],[11] } ->{ ignore, [B],[A],[AB]}
I hope this helps! if it did, please hit the checkmark to this anwer
Run Sub Test:
Sub Test()
Dim bCondSatisfied As Boolean
Dim InxComb As Integer
Dim InxResult As Integer
Dim count As Integer
Dim i As Integer
Dim j As Integer
Dim arr() As String
Dim TestData() As Variant
Dim Result() As Variant
Dim Result_filtered() As Variant
TestData = Array(1, 3, 2, 4)
Call GenerateCombinations(TestData, Result)
'Now you have all the possible combinations, you can apply custom conditions
'(e.g. any number on the left side of another number should be smaller, practically this is satisfied with the
' given test array, but if the numbers are scrambled it will fix the problem)
ReDim Result_filtered(0 To 0)
Result_filtered(0) = "No Combination Matched the Condition" 'default for the case there is no result matched
count = 0
For i = 0 To UBound(Result)
arr() = Result(i)
bCondSatisfied = True
If UBound(arr) > 0 Then 'if there is more than one number in the array, compare the adjacent numbers
For j = 0 To UBound(arr) - 1
If arr(j) > arr(j + 1) Then
bCondSatisfied = False
Exit For
End If
Next j
End If
'Store the array in the filtered array if it passed the test
If bCondSatisfied = True Then
ReDim Preserve Result_filtered(count)
Result_filtered(count) = arr
count = count + 1
End If
Next i
'Print Result
For InxResult = 0 To UBound(Result)
Debug.Print Right(" " & InxResult + 1, 3) & " ";
For InxComb = 0 To UBound(Result(InxResult))
Debug.Print "[" & Result(InxResult)(InxComb) & "] ";
Next
Debug.Print
Next
Debug.Print "-----------------" 'separate two results
'Print Result_filtered
For InxResult = 0 To UBound(Result_filtered)
Debug.Print Right(" " & InxResult + 1, 3) & " ";
For InxComb = 0 To UBound(Result_filtered(InxResult))
Debug.Print "[" & Result_filtered(InxResult)(InxComb) & "] ";
Next
Debug.Print
Next
End Sub
Sub GenerateCombinations(ByRef AllFields() As Variant, _
ByRef Result() As Variant)
Dim InxResultCrnt As Integer
Dim InxField As Integer
Dim InxResult As Integer
Dim i As Integer
Dim NumFields As Integer
Dim Powers() As Integer
Dim ResultCrnt() As String
NumFields = UBound(AllFields) - LBound(AllFields) + 1
ReDim Result(0 To 2 ^ NumFields - 2) ' one entry per combination
ReDim Powers(0 To NumFields - 1) ' one entry per field name
' Generate powers used for extracting bits from InxResult
For InxField = 0 To NumFields - 1
Powers(InxField) = 2 ^ InxField
Next
For InxResult = 0 To 2 ^ NumFields - 2
' Size ResultCrnt to the max number of fields per combination
' Build this loop's combination in ResultCrnt
ReDim ResultCrnt(0 To NumFields - 1)
InxResultCrnt = -1
For InxField = 0 To NumFields - 1
If ((InxResult + 1) And Powers(InxField)) <> 0 Then
' This field required in this combination
InxResultCrnt = InxResultCrnt + 1
ResultCrnt(InxResultCrnt) = AllFields(InxField)
End If
Next
' Discard unused trailing entries
ReDim Preserve ResultCrnt(0 To InxResultCrnt)
' Store this loop's combination in return array
Result(InxResult) = ResultCrnt
Next
End Sub

VBA Excel Finding and Combining Rows Based on Matching Column Cells

I'm trying to figure out a way to combine rows based on values in two specific columns in vba excel.
For Example:
Let's say I have the following sheet:
Column A Column J Column Z
1 A ?
1 A !
2 B ?
2 B !
And I need to convert it to this:
Column A Column J Column Z
1 A ?, !
2 B ?, !
Here's another method using User Defined Types and collections to iterate through the list and develop the combined results. For large sets of data, it should be considerably faster than reading through each cell on the worksheet.
I assume that you are grouping on Col J, and that Column A data does not need to be concatenated in the cell. If it does, the modifications to the routine would be trivial.
First, Insert a Class Module, rename it CombData and insert the following code into that module:
Option Explicit
Private pColA As String
Private pColJ As String
Private pColZConcat As String
Public Property Get ColA() As String
ColA = pColA
End Property
Public Property Let ColA(Value As String)
pColA = Value
End Property
Public Property Get ColJ() As String
ColJ = pColJ
End Property
Public Property Let ColJ(Value As String)
pColJ = Value
End Property
Public Property Get ColZConcat() As String
ColZConcat = pColZConcat
End Property
Public Property Let ColZConcat(Value As String)
pColZConcat = Value
End Property
Then Insert a Regular Module and insert the Code Below:
Option Explicit
Sub CombineData()
Dim cCombData As CombData
Dim colCombData As Collection
Dim V As Variant
Dim vRes() As Variant 'Results Array
Dim rRes As Range 'Location of results
Dim I As Long
'read source data into array
V = Range("A1", Cells(Rows.Count, "A").End(xlUp)).Resize(columnsize:=26)
'Set results range. Here it is set below the Source Data
'Could be anyplace, even on a different worksheet; or could overlay the
' original. Area below and to right is cleared
Set rRes = Range("A1").Offset(UBound(V) + 10)
Range(rRes, rRes.SpecialCells(xlCellTypeLastCell)).Clear
Set colCombData = New Collection
On Error Resume Next
For I = 1 To UBound(V)
Set cCombData = New CombData
cCombData.ColA = V(I, 1)
cCombData.ColJ = V(I, 10)
cCombData.ColZConcat = V(I, 26)
colCombData.Add cCombData, CStr(cCombData.ColJ)
If Err.Number <> 0 Then
Err.Clear
With colCombData(cCombData.ColJ)
.ColZConcat = .ColZConcat & ", " & V(I, 26)
End With
End If
Next I
On Error GoTo 0
ReDim vRes(1 To colCombData.Count, 1 To 26)
For I = 1 To UBound(vRes)
With colCombData(I)
vRes(I, 1) = .ColA
vRes(I, 10) = .ColJ
vRes(I, 26) = .ColZConcat
End With
Next I
rRes.Resize(UBound(vRes, 1), UBound(vRes, 2)) = vRes
End Sub
EDIT: Note that the source data is read into the Variant array V. If you examine V in the Watch Window, you will see that the first dimension represents the rows; and the second dimension the columns. So if you wanted, for example, to perform the same procedure on a different set of columns, you would merely change the references to the second dimension under the line that reads Set cCombData = New CombData. For example, column B data would be V(I,2), and so forth. Of course, you might want to rename the data types to make them more representative of what you are doing.
In addition, if your data starts at row 2, merely start the iteration through V with I = 2 instead of I = 1.
EDIT2: In order to both overwrite the original, and also maintain the contents of the columns not being processed, the following modification will do that for Columns A, J and Z. You should be able to modify it for whatever columns you choose to process.
Option Explicit
Sub CombineData()
Dim cCombData As CombData
Dim colCombData As Collection
Dim V As Variant
Dim vRes() As Variant 'Results Array
Dim rRes As Range 'Location of results
Dim I As Long, J As Long, K As Long
'read source data into array
V = Range("A1", Cells(Rows.Count, "A").End(xlUp)).Resize(columnsize:=26)
'Set results range. Here it is set below the Source Data
'Could be anyplace, even on a different worksheet; or could overlay the
' original. Area below and to right is cleared
Set rRes = Range("A1") '.Offset(UBound(V) + 10)
Range(rRes, rRes.SpecialCells(xlCellTypeLastCell)).Clear
Set colCombData = New Collection
On Error Resume Next
For I = 1 To UBound(V)
Set cCombData = New CombData
cCombData.ColA = V(I, 1)
cCombData.ColJ = V(I, 10)
cCombData.ColZConcat = V(I, 26)
colCombData.Add cCombData, CStr(cCombData.ColJ)
If Err.Number <> 0 Then
Err.Clear
With colCombData(cCombData.ColJ)
.ColZConcat = .ColZConcat & ", " & V(I, 26)
End With
End If
Next I
On Error GoTo 0
ReDim vRes(1 To colCombData.Count, 1 To 26)
For I = 1 To UBound(vRes)
With colCombData(I)
vRes(I, 1) = .ColA
vRes(I, 10) = .ColJ
vRes(I, 26) = .ColZConcat
'Note the 10 below is the column we are summarizing by
J = WorksheetFunction.Match(.ColJ, WorksheetFunction.Index(V, 0, 10), 0)
For K = 1 To 26
Select Case K 'Decide which columns to copy over
Case 2 To 9, 11 To 25
vRes(I, K) = V(J, K)
End Select
Next K
End With
Next I
rRes.Resize(UBound(vRes, 1), UBound(vRes, 2)) = vRes
End Sub
This is assuming that Column J is the key and Column A doesn't need to be appended. If Column A needs to be combined as well (not always the same), you would simply need to add another for each loop to check if the data is there, and add it if not, as done for col 26 in the code.
Sub CombineData()
x = 2
Do Until Cells(x, 1) = "" 'loop through every row in sheet starting at 2 (1 will never be removed, since it is the first data)
x2 = 1
Do Until x2 = x
If Cells(x, 10) = Cells(x2, 10) Then 'this is comparing column J. If another column is the reference then change 10 to the column number
splt = Split(Cells(x, 26), ", ")
For Each s In splt 'check to see if data already in column z
If s = Cells(x2, 26) Then GoTo alreadyEntered
Next
Cells(x, 26) = Cells(x, 26) & ", " & Cells(x2, 26) 'append column z data to row x
alreadyEntered:
Rows(x2).Delete Shift:=xlUp 'delete duplicate row
x = x - 1 'to keep x at same row, since we just removed a row
Exit Do
Else
x2 = x2 + 1
End If
Loop
x = x + 1
Loop
End Sub

Excel/VBA Breakdown field

Bit of a complicated task I have to do but I will try and explain. I have an excel file with 23000 lines of data which I am importing into a website. Each one has a field like so:
Category | other data | other data 2
Foods/Dog/Treats Pre-Pack | 1223 | image.jpg
I need it to grab each line and add a new line below it for each "/" so turning the above into:
Category | other data | other data 2
[blank in original line] | 1223 | image.jpg
Foods | [blank field] | [blank field]
Foods/Dog | [blank field] | [blank field]
Foods/Dog/Treats Pre-Pack | [blank field] | [blank field]
So the script needs to add a new line for each category but keeping the original category in front of it. So turning category/category2/category 3 into 4 lines of: [blank] - category - category/category2 - category/category2/category 3
Does anyone know a way or script to do this?
Thanks, Simon
Note: The worksheet is called "test" and the category column starts at E2 and goes to E23521
I have the following script:
Sub test()
Dim a, i As Long, ii As Long, e, n As Long
Dim b(), txt As String, x As Long
With Range("a1").CurrentRegion
a = .Value
txt = Join$(Application.Transpose(.Columns(5).Value))
With CreateObject("VBScript.RegExp")
.Global = True
.Pattern = "/"
x = .Execute(txt).Count * 2
End With
ReDim b(1 To UBound(a, 1) + x, 1 To UBound(a, 2))
For i = 1 To UBound(a, 1)
If a(i, 5) <> "" Then
For Each e In Split(a(i, 5), "/")
n = n + 1
For ii = 1 To UBound(a, 2)
b(n, ii) = a(i, ii)
Next
b(n, 5) = Trim$(e)
Next
End If
Next
.Resize(n).Value = b
End With
End Sub
This seems to create a new row as I need it to but does not keep the slash structuring moving up with each one. And also dosnt add a blank line on all the new ones and make the original category value blank.
SOLVED:
Sub splitEmUp()
Dim splitter() As String 'this is storage space for the split function
Dim i As Integer ' main-loop for counter "which cell we are on"
Dim j As Integer ' splitter for-loop counter "which section of the split are we on"
Range("E2").Activate 'starting in cell e2 because row 1 is headers and category is located in the B column
For i = 0 To 24000 'from beginning to end i=0 means e2, i=1 means e3
ActiveCell.Offset(i, 0).Value = Replace(ActiveCell.Offset(i, 0).Value, " / ", "!##")
splitter = Split(ActiveCell.Offset(i, 0), "/") 'split the cell based on / and store it in splitter
If (UBound(splitter)) > 0 Then 'if a split occurred
ActiveCell.Offset(i, 0).Value = "" 'set the activecell to blank
Debug.Print i
ActiveCell.Offset(i + 1, 0).EntireRow.Insert shift:=xlDown, copyorigin:=xlFormatFromLeftOrAbove 'insert a new row and shift everything down
ActiveCell.Offset(i + 1, 0).Value = splitter(0) 'initialize the "Down" cells
ActiveCell.Offset(i + 1, 0).Value = Replace(ActiveCell.Offset(i + 1, 0).Value, "!##", " / ")
For j = 1 To UBound(splitter)
ActiveCell.Offset(i + j + 1).EntireRow.Insert shift:=xlDown, copyorigin:=xlFormatFromLeftOrAbove 'create another row if it needs to
ActiveCell.Offset(i + (j + 1), 0).Value = ActiveCell.Offset(i + j).Value & "/" & splitter(j) 'fill out the new row
ActiveCell.Offset(i + (j + 1), 0).Value = Replace(ActiveCell.Offset(i + (j + 1), 0).Value, "!##", " / ")
Next
i = i + UBound(splitter) + 1 'need to step I past the new cells
ReDim splitter(0)
Erase splitter 'erase and eliminate splitter to avoid carry over.
End If
Next
End Sub
This is what I came up with. Be sure to change the sheet names to fit your workbook. Also be sure to change the input range to fit your own input range of cells.
Function SplitAndWrite(inputCell As Range, TopOfOutputRange As Range, sep As String) As Range
Dim texts() As String, i As Integer, outputText As String
texts = Split(inputCell.Value, sep)
outputText = ""
TopOfOutputRange = "" 'your blank line
For i = LBound(texts) To UBound(texts)
outputText = outputText & sep & texts(i)
TopOfOutputRange.Offset(i + 1) = outputText
Next i
Set SplitAndWrite = TopOfOutputRange.Offset(UBound(texts) + 1)
End Function
Sub THEPOPULATOR()
Dim s3 As Worksheet, s4 As Worksheet
Set s3 = Sheets("Sheet1")
Set s4 = Sheets("Sheet2")
Dim inputrange As Range, c As Range, outputrange As Range
Set outputrange = s4.Range("A1")
Set inputrange = s3.Range(s3.Cells(2, 1), s3.Cells(2, 1).End(xlDown)) 'cells(2,1) = "A1". change this to your top input cell. then the second half will find the bottom cell on its own. This is the same as pressing Ctrl+down
For Each c In inputrange
s3.Range(c.Offset(0, 1), c.Offset(0, c.End(xlToRight).Column)).Copy outputrange.Offset(1, 1)
Set outputrange = SplitAndWrite(c, outputrange.Offset(1), "/")
Next c
End Sub
Here is an example from another solution How to split cell in a row with Excel, which I modified just a tiny bit to fit your situation:
Public Sub solutionJook()
Dim arr() As Variant
Dim arrSum() As Variant
Dim arrResult() As Variant
Dim arrTemp As Variant
Dim i As Long
Dim j As Long
Dim h As Long
Dim lngSplitColumn As Long
'input of array to seperate -> should cover all columns+rows of your data
arr = Range("A1:C2")
'specify which column has the values to be split up -> here this is the category column
lngSplitColumn = 2
'using the boundries of the given range,
'arrSum has now always the right boundries for the first dimension
ReDim Preserve arrSum(LBound(arr, 2) To UBound(arr, 2), 1 To 1)
'create the array with seperated A B C
For i = LBound(arr, 1) To UBound(arr, 1)
'use split to make Foods/Dog/Treats Pre-Pack into an array, using '\' (chr(92)) as indicator
arrTemp = Split(arr(i, lngSplitColumn), Chr(92))
'every value of arrTemp creates a new row
For j = LBound(arrTemp) To UBound(arrTemp)
'loop through all input columns and create the new row
For h = LBound(arr, 2) To UBound(arr, 2)
If h = lngSplitColumn Then
'setup the value of the splitted column
Dim k as long
arrSum(h, UBound(arrSum, 2)) = arrTemp(LBound(arrTemp))
for k = LBound(arrTemp)+1 to j
arrSum(h, UBound(arrSum, 2)) = arrSum(h, UBound(arrSum, 2)) & "\" & arrTemp(k) 'set Foods Foods/Dog Foods/Dog/Treats Pre-Pack
next k
Else
'setup the value of any other column
arrSum(h, UBound(arrSum, 2)) = arr(i, h) 'set Value of Column h
End If
Next h
ReDim Preserve arrSum(LBound(arr, 1) To UBound(arr, 2), _
LBound(arrSum, 2) To (UBound(arrSum, 2) + 1))
Next j
Next i
'clean up last empty row (not realy necessary)
ReDim Preserve arrSum(LBound(arr, 1) To UBound(arr, 2), _
LBound(arrSum, 2) To (UBound(arrSum, 2) - 1))
'setup transposed result array
ReDim arrResult(LBound(arrSum, 2) To UBound(arrSum, 2), _
LBound(arrSum, 1) To UBound(arrSum, 1))
'transpose the array
For i = LBound(arrResult, 1) To UBound(arrResult, 1)
For j = LBound(arrResult, 2) To UBound(arrResult, 2)
arrResult(i, j) = arrSum(j, i)
Next j
Next i
'specify target range
Range(Cells(1, 5), Cells(UBound(arrResult, 1), 4 + UBound(arrResult, 2))) = arrResult
End Sub
You might need to adapt the target range however.
Cells(1,5) -> E1 is the starting point of pasting