Changing two tables into a matrix - vba

I have 2 spreadsheets with the data below.
Name | System 1 | System 2 | System 3 |
John | x | x | |
James| | x | x |
Peter| | x | |
Name | Process A | Process B | Process C |
John | | x | |
James| x | | x |
Peter| x | | x |
Are there any ways in VBA I can do to merge these two lists in a matrix format as below?
| Process A | Process B | Process C |
System 1 | | John | |
System 2 | James, Peter | John | James, Peter |
System 3 | James | | James |
I have coding experience but not very strong in VBA. Appreciate if anyone can give me some code samples to start with.
There are 27 systems, 21 processes and 188 names. So, it will take sometime doing it manually.
Thank you.

Comments are in the code, HTH.
Option Explicit
Sub Main(): On Error GoTo errMain
Dim system As Range
Dim process As Range
' Select ranges of systems and processes
Set system = Application.InputBox( _
prompt:="Go to sheet with 'system' data and select it", Title:="S Y S T E M", Type:=8)
Set process = Application.InputBox( _
prompt:="Go to sheet with 'process' data and select it", Title:="P R O C E S S", Type:=8)
' Do the merge
MergeIt system, process
Exit Sub
errMain:
MsgBox Err.Description, vbCritical
End Sub
Private Sub MergeIt(system As Range, process As Range)
Dim processData As Range
Dim processColumn As Range
Dim processName As String
Dim processUsers As Variant
Dim processValues As Variant
Dim processIndex As Integer
Dim systemData As Range
Dim systemColumn As Range
Dim systemName As String
Dim systemUsers As Variant
Dim systemValues As Variant
Dim systemIndex As String
' Add new sheet where the merged data will be stored
Dim mergedSheet As Worksheet
Set mergedSheet = Worksheets.Add(after:=Worksheets(Worksheets.Count))
mergedSheet.Name = "Merged" & _
Year(Now) & Month(Now) & Day(Now) & Hour(Now) & Minute(Now) & Second(Now)
' Get process and system users as first column without the first cell
processUsers = process.Columns(1).Offset(1, 0).Resize(process.Rows.Count - 1, 1)
systemUsers = system.Columns(1).Offset(1, 0).Resize(system.Rows.Count - 1, 1)
' Get process and system data as all columns except the first one where the users are
Set processData = process.Offset(0, 1).Resize(process.Rows.Count, process.Columns.Count - 1)
Set systemData = system.Offset(0, 1).Resize(system.Rows.Count, system.Columns.Count - 1)
processIndex = 1
' Go the process data by columns.
' Add process name to result sheet and for each process column go through
' all system columns and do the merge
For Each processColumn In processData.Columns
processIndex = processIndex + 1
processName = processColumn.Cells(1).Value
mergedSheet.Rows(1).Cells(processIndex).Value = processName
processValues = processColumn.Offset(1, 0).Resize(processColumn.Rows.Count - 1, 1)
systemIndex = 1
For Each systemColumn In systemData.Columns
systemIndex = systemIndex + 1
systemValues = systemColumn.Offset(1, 0).Resize(systemColumn.Rows.Count - 1, 1)
If mergedSheet.Columns(1).Cells(systemIndex).Value = "" Then
systemName = systemColumn.Cells(1).Value
mergedSheet.Columns(1).Cells(systemIndex).Value = systemName
End If
mergedSheet.Cells(systemIndex, processIndex).Value = _
IntersectOfValues(processUsers, processValues, systemUsers, systemValues)
Next systemColumn
Next processColumn
End Sub
Private Function IntersectOfValues( _
ByVal processUsers As Variant, _
ByVal processValues As Variant, _
ByVal systemUsers As Variant, _
ByVal systemValues As Variant) As String
Dim i As Integer
Dim j As Integer
' Go through all process and system values.
' Compare names which correspond to values.
' Append the name to result if it was found in both process and system values.
For i = LBound(processValues) To UBound(processValues)
If Trim(processValues(i, 1)) = "" Then _
GoTo nextI
For j = LBound(systemValues) To UBound(systemValues)
If Trim(systemValues(j, 1)) = "" Then _
GoTo nextJ
If systemUsers(j, 1) = processUsers(i, 1) Then
IntersectOfValues = IntersectOfValues & processUsers(i, 1) & ","
Exit For
End If
nextJ:
Next j
nextI:
Next i
If Len(IntersectOfValues) = 0 Then _
Exit Function
If Right(IntersectOfValues, 1) = "," Then _
IntersectOfValues = Left(IntersectOfValues, Len(IntersectOfValues) - 1)
End Function

Related

Excel - across and down matches

I have a workbook with two sheets in it, the first looks like this:
--------------------------------------------------------
Last Name | First Name | 1-Jan | 2-Jan | 3-Jan | 4-Jan | (continues on like this)
--------------------------------------------------------
SMITH | John | 1 | 1 | | |
--------------------------------------------------------
BOND | James | | | 1 | 1 |
--------------------------------------------------------
Second sheet
--------------------------------------------------------
| January | February | (continues on etc)
--------------------------------------------------------
Last Name | First Name | From | To | From | To |
--------------------------------------------------------
SMITH | John |1/1/18 | 2/2/18| | |
--------------------------------------------------------
BOND | James |3/1/18 |4/1/18 | | |
--------------------------------------------------------
This is a leave sheet and basically the user inputs on the first sheet a '1' in the day where they are taking leave. This is then automatically updated in the second sheet to reflect the inclusive dates of their leave for each month.
So in the first example, user enters 1 in 1-Jan and 2-Jan, this updates second sheet with leave for that employee from 1/1/18 to 2/1/18.
So far, I'm successful in being able to detect when a 1 is entered it grabs the name and date details, I've been using a msgbox to verify that I'm getting the right data.
The problem I'm having is that's as far as I can get, I can't work out how to search the second sheet to find the dates and update accordingly.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim KeyCells As Range
' The variable KeyCells contains the cells that will
' cause an alert when they are changed.
Set KeyCells = Range("D6:OI53")
If Not Application.Intersect(KeyCells, Range(Target.Address)) _
Is Nothing Then
' If cell changed, do the below '
' Get name '
Dim lastName As String
Dim firstName As String
lastName = ActiveSheet.Cells(Target.Cells.Row, 1).Value
firstName = ActiveSheet.Cells(Target.Cells.Row, 2).Value
'Get date '
Dim leaveDate As String
leaveDate = ActiveSheet.Cells(5, Target.Cells.Column).Value
' Test lastname, firstname, date '
UpdateMonthlyLeave lastName, firstName, leaveDate
End If
End Sub
Sub UpdateMonthlyLeave(lastName As String, firstName As String, leaveDate As String)
MsgBox lastName & " " & firstName & " " & leaveDate
' Find employee on monthly leave sheet '
End Sub
This UDF will return a list of start OR end dates. Just make you you select Wrap Text for the columns in sheet 2. I think one advantage may be Excel would only update the cell with the formula if the ranges it specifies are changed.
Perhaps the code could be simplified further, but unfortunately you will have to enter the formula for each cell in sheet2.
Option Explicit
' ShowStartMonth: True If we need to return the start date of the holidays
' MonthRange: The WHOLE Column range of the Month
' RowRange: The Range of the person's row but only the holiday columns, not the name columns
' MonthNameRow: The entire row of where the Month name is
Public Function GetHoliday(ShowStartMonth As Boolean, iMonth As Integer, RowRange As Range, MonthNameRow As Range) As String
Dim MonthRange As Range
Set MonthRange = GetMonthRange(iMonth, MonthNameRow)
'Init variables
' Get the cells for the current month
Dim rRow As Range
Set rRow = Intersect(RowRange, RowRange.Worksheet.UsedRange, MonthRange)
Dim IsCurrentCellHoliday As Boolean
Dim IsLastCellHoliday As Boolean
Dim IsStartHolidayContinuation As Boolean
' If First Day of month is a holiday and last day of last month is a holiday then
' Holiday is continuation
IsStartHolidayContinuation = (rRow.Cells(1).Value = 1) And (rRow.Cells(1).Offset(0, -1).Value = 1)
IsLastCellHoliday = (rRow.Cells(1).Value = 1)
' These will hold the dates for start or end of a holiday
Dim StartDays() As String
Dim EndDays() As String
ReDim StartDays(0 To 255)
ReDim EndDays(0 To 255)
Dim SDIndex As Integer ' Index of the start day array
Dim EDIndex As Integer ' Index of the end day array
' If Start of month is start of a new holiday then set it
If (IsLastCellHoliday And Not IsStartHolidayContinuation) Then StartDays(0) = GetMonthName(rRow.Cells(1), MonthNameRow)
' If start of month is a holiday then set index to the second "StartHoliday" line
SDIndex = IIf(IsStartHolidayContinuation Or IsLastCellHoliday, 1, 0) ' Keep first row Empty if start of month is holiday
EDIndex = 0
' Loop through all cells in the month for the person
Dim i As Integer
For i = SDIndex + 1 To rRow.Columns.Count
Dim rCell As Range
Set rCell = rRow.Cells(i)
IsCurrentCellHoliday = rCell.Value = 1 'Check if current cell is a holiday
' If the current cell is different to the last cell then we need to do something
If IsCurrentCellHoliday <> IsLastCellHoliday Then
If IsCurrentCellHoliday Then
StartDays(SDIndex) = GetMonthName(rCell, MonthNameRow)
SDIndex = SDIndex + 1
' Check if the first day of the next month is a holiday, if not then today is the last day
If rCell.Column = MonthRange.Columns(MonthRange.Columns.Count).Column And rCell.Offset(0, 1).Value <> 1 Then
EndDays(EDIndex) = GetMonthName(rRow.Cells(i), MonthNameRow)
EDIndex = EDIndex + 1
End If
Else
EndDays(EDIndex) = GetMonthName(rRow.Cells(i - 1), MonthNameRow)
EDIndex = EDIndex + 1
End If
End If
IsLastCellHoliday = IsCurrentCellHoliday
Next
Dim ReturnStrings() As String
Dim ReturnIndex As Integer
If (ShowStartMonth) Then
ReturnStrings = StartDays
ReturnIndex = SDIndex
Else
ReturnStrings = EndDays
ReturnIndex = EDIndex
End If
Dim returnString As String
returnString = IIf(Len(ReturnStrings(0)) = 0, " - ", ReturnStrings(0))
Dim j As Integer
For j = LBound(ReturnStrings) + 1 To ReturnIndex - 1
returnString = returnString & vbNewLine & ReturnStrings(j)
Next
GetHoliday = returnString
End Function
Private Function GetMonthName(cell As Range, MonthRow As Range) As String
Dim rMonth As Range
Set rMonth = Intersect(cell.EntireColumn, MonthRow.EntireRow)
End Function
Public Function GetMonthRange(iMonth As Integer, MonthNameRow As Range) As Range
Set MonthNameRow = Intersect(MonthNameRow.EntireRow, MonthNameRow.Worksheet.UsedRange)
Dim startCell As Range
Dim endCell As Range
Dim rCell As Range
For Each rCell In MonthNameRow.Cells
If IsDate(rCell.Value) Then
If month(CDate(rCell.Value)) = iMonth Then
If startCell Is Nothing Then
Set startCell = rCell
ElseIf rCell.Column < startCell.Column Then
Set startCell = rCell
End If
If endCell Is Nothing Then
Set endCell = rCell
ElseIf rCell.Column > endCell.Column Then
Set endCell = rCell
End If
End If
End If
Next
Set GetMonthRange = Range(startCell.Address & ":" & endCell.Address).EntireColumn
Dim sAddress As String
sAddress = GetMonthRange.Address
End Function

VBA - Count Rows then in Column C then fill Columns A, B and P with Equations

I have 5 columns. Column C "Account" and column D "Person" are my data set.
I want to use VBA to look at how many rows of data I have and then fill that number of rows in 1) Column E "Concatenate" with a concatenate of "Account" and "Employee" fill that number of rows in 2) columns A and B with an INDEX-MATCH equation I have.
..I tried to draw out my columns below but it isn't formatting in the way I'd like it to be... sorry
Owner | Comment | Account | Employee | Concatenate
Jay | Done | JSMA1 | Sally | JSMA1 Sally
Will | Not Done| KLLM4 | Jack | KLLM4 Jack
Ken | Done | BM3R1 | Sam | BM3R1 Sam
Any ideas?
try this:
Option Explicit
Public Sub fillRanges()
Dim ur As Range, hdr As Range, conCol As Variant, lRow As Long
Dim ownCol As Variant, comCol As Variant
Dim actCol As Variant, empCol As Variant
Set ur = Sheet1.UsedRange ' minimal range
Set hdr = ur.Rows(1) ' header row
lRow = ur.Rows.Count ' last row
With Application
ownCol = .Match("Owner", hdr, 0)
comCol = .Match("Comment", hdr, 0)
actCol = .Match("Account", hdr, 0)
empCol = .Match("Employee", hdr, 0)
conCol = .Match("Concatenate", hdr, 0)
End With
If Not IsError(ownCol) And _
Not IsError(comCol) And _
Not IsError(actCol) And _
Not IsError(empCol) And _
Not IsError(conCol) _
Then
With ur
.Range(.Cells(2, ownCol), .Cells(lRow, ownCol)) = "INDEX-MATCH equation 1"
.Range(.Cells(2, comCol), .Cells(lRow, comCol)) = "INDEX-MATCH equation 2"
.Range(.Cells(2, conCol), .Cells(lRow, conCol)).Formula = _
"=INDIRECT(ADDRESS(ROW()," & actCol & ")) & "" "" & " & _
" INDIRECT(ADDRESS(ROW(), " & empCol & "))"
End With
End If
End Sub

Concatenate Only the Visible Rows

I am using the following VBA to concatenate rows with a common ID
Function JoinAll(ByVal BaseValue, ByRef rng As Range, ByVal delim As String)
Dim a, i As Long
a = rng.Value
For i = 1 To UBound(a, 1)
If a(i, 1) = BaseValue Then JoinAll = JoinAll & _
IIf(JoinAll = "", "", delim) & a(i, 3)
Next
End Function
As an example:
ID | Date | Purchase | Concat Value
1 | 3/4/16 | Car | Car, Cap
2 | 5/2/12 | Cat | Cat
1 | 6/2/13 | Cap | Cap
When run, this creates Car, Cap.
However, this is a table with a filter, and once it is filtered to this:
ID | Date | Purchase | Concat Value
1 | 3/4/16 | Car | Car, Cap
2 | 5/2/12 | Cat | Cat
It still shows Car, Cap instead of ignoring that Cap is not visible.
I have seen this answer, but don't see how to make it work with my current VBA:
Excel VBA Concatenate only visible cells of filtered column. Test code included
UPDATE:
Using this I am getting only the visible items joined, but I need it to return the values in column 3. This only returns the values in column 1:
Function JoinAll(ByVal BaseValue, ByRef rng As Range, ByVal delim As String)
For Each a In rng
If a = BaseValue And a.EntireRow.Hidden = False Then
JoinAll = JoinAll & IIf(JoinAll = "", "", delim) & a
End If
Next a
End Function
This works. There is a typo/bug in your original code as a=rng.value, so a should be rng when considering the hidden rows.
Function JoinAll3(ByVal BaseValue, ByRef rng As Range, ByVal delim As String)
Dim a, i As Long
a = rng.Value
For i = 1 To UBound(a, 1)
If a(i, 1) = BaseValue And rng(i, 1).EntireRow.Hidden = False Then
JoinAll3 = JoinAll3 & IIf(JoinAll3 = "", "", delim) & a(i, 3)
End If
Next
End Function
Have you tried something like:
For each val in rng.Columns(3).Cells
If val = BaseValue And val.EntireRow.Hidden = False Then
JoinAll = JoinAll & IIf(JoinAll = "", "", delim) & val
End If
Next val

Creating a unique entry for each line item in Excel

I need help in creating a macro in Excel wherein it grabs a certain cell and copies the entire row x number of times depending on the cell's contents.
To make it clear, let's say I have 2 rows:
| Order # | Item | Qty |
| 30001 | bag | 3 |
| 30002 | pen | 1 |
What I want the macro to do is grab the number under the Qty column and copy the entire row and insert a new line with the exact same contents under it. The number of times it does this depends on the number in the Qty cell. Also, it appends a three digit number in the Order # cell to make it a unique reference point. What the end-result should be:
| Order # | Item | Qty |
| 30001-001 | bag | 1 |
| 30001-002 | bag | 1 |
| 30001-003 | bag | 1 |
| 30002-001 | pen | 1 |
It's hard to explain it here but I hope you get the point. Thanks in advance, gurus!
The following code supports blank lines in the middle of the data.
If Qty = 0, it won't write the Item in the output table.
Please insert at least 1 row of data, because it won't work if there is no data :)
Option Explicit
Sub caller()
' Header at Row 1:
' "A1" = Order
' "B1" = Item
' "C1" = Qty
'
' Input Data starts at Row 2, in "Sheet1"
'
' Output Data starts at Row 2, in "Sheet2"
'
' Sheets must be manually created prior to running this program
Call makeTheThing(2, "Sheet1", "Sheet2")
End Sub
Sub makeTheThing(lStartRow As Long, sSheetSource As String, sSheetDestination As String)
Dim c As Range
Dim rOrder As Range
Dim sOrder() As String
Dim sItem() As String
Dim vQty As Variant
Dim sResult() As String
Dim i As Long
' Reads
With ThisWorkbook.Sheets(sSheetSource)
Set rOrder = .Range(.Cells(lStartRow, 1), .Cells(.Cells(.Rows.Count, 1).End(xlUp).Row, 1)) ' It will work if there are blank lines in the middle!
i = rOrder.Rows.Count
ReDim sOrder(1 To i)
ReDim sItem(1 To i)
ReDim vQty(1 To i)
i = 1
For Each c In rOrder
sOrder(i) = Trim(c.Text)
sItem(i) = Trim(c.Offset(0, 1).Text)
vQty(i) = c.Offset(0, 2).Value
i = i + 1
Next c
End With
' Processes
sResult = processData(sOrder, sItem, vQty)
' Writes
ThisWorkbook.Sheets(sSheetDestination).Range("A" & lStartRow).Resize(UBound(sResult, 1), UBound(sResult, 2)).Value = sResult
End Sub
Function processData(sOrder() As String, sItem() As String, vQty As Variant) As String()
Dim i As Long
Dim j As Long
Dim k As Long
Dim sResult() As String
j = WorksheetFunction.Sum(vQty) ' That's why vQty had to be Variant!
ReDim sResult(0 To j, 1 To 3)
k = 0
For i = 1 To UBound(sOrder)
For j = 1 To vQty(i)
sResult(k, 1) = sOrder(i) & "-" & Format(j, "000")
sResult(k, 2) = sItem(i)
sResult(k, 3) = "1"
k = k + 1
Next j
Next i
processData = sResult
End Function
I hope it helps you. I had fun making it!
One way: Walk down the qty column inserting as needed then jumping to the next original row;
Sub unwind()
Dim rowCount As Long, cell As Range, order As String, i As Long, r As Long
Set cell = Range("C1")
rowCount = Range("C" & rows.Count).End(xlUp).Row
For i = 1 To rowCount
order = cell.Offset(0, -2).Value
For r = 0 To cell.Value - 1
If (r > 0) Then cell.Offset(r).EntireRow.Insert
cell.Offset(r, 0).Value = 1
cell.Offset(r, -1).Value = cell.Offset(0, -1).Value
cell.Offset(r, -2).Value = order & "-" & Format$(r + 1, "000")
Next
Set cell = cell.Offset(r, 0)
Next
End Sub

Excel VBA to create every possible combination (without repetition)

i need help with the following excel and what looks like a VBA problem.
The idea here is to generate all the possible combination (without repetition) in each grouping.
INPUT
COLUMN A | COLUMN B
A | 1
X | 1
D | 1
C | 2
E | 2
OUTPUT
COLUMN A | COLUMN B
A | X
A | D
X | D
X | A
D | A
D | X
C | E
E | C
What I managed to do.... how do i let it run only if the data is in the same group.
Option Explicit
Sub Sample()
Dim i As Long, j As Long
Dim CountComb As Long, lastrow As Long
Application.ScreenUpdating = False
CountComb = 0: lastrow = 1
For i = 1 To 10: For j = 1 To 10
Range("G" & lastrow).Value = Range("A" & i).Value & "/" & _
Range("B" & j).Value
lastrow = lastrow + 1
CountComb = CountComb + 1
Next: Next
Application.ScreenUpdating = True
End Sub
see below. Note you need to add the reference Microsoft Scripting Runtime in Tools >> References. Change the Range("A1:A5") to either a dynamic named range or static range and the routine will handle the rest for you. It displays the results starting in G1 but you can also change this / make dynamic as an offset from the data range. Up to you.
Option Explicit
Option Base 1
Dim Data As Dictionary
Sub GetCombinations()
Dim dataObj As Variant
Dim returnData As Variant
Set Data = New Dictionary
Dim i As Double
dataObj = Range("A1:B5").Value2
' Group Data
For i = 1 To UBound(dataObj) Step 1
If (Data.Exists(dataObj(i, 2))) Then
Data(dataObj(i, 2)) = Data(dataObj(i, 2)) & "|" & dataObj(i, 1)
Else
Data.Add dataObj(i, 2), dataObj(i, 1)
End If
Next i
' Extract combinations from groups
returnData = CalculateCombinations().Keys()
Range("G1").Resize(UBound(returnData) + 1, 1) = Application.WorksheetFunction.Transpose(returnData)
End Sub
Private Function CalculateCombinations() As Dictionary
Dim i As Double, j As Double
Dim datum As Variant, pieceInner As Variant, pieceOuter As Variant
Dim Combo As New Dictionary
Dim splitData() As String
For Each datum In Data.Items
splitData = Split(datum, "|")
For Each pieceOuter In splitData
For Each pieceInner In splitData
If (pieceOuter <> pieceInner) Then
If (Not Combo.Exists(pieceOuter & "|" & pieceInner)) Then
Combo.Add pieceOuter & "|" & pieceInner, vbNullString
End If
End If
Next pieceInner
Next pieceOuter
Next datum
Set CalculateCombinations = Combo
End Function