calculating average based on condition - excel

I am trying to put together a formula that gives me the average total for an order if the order contains a specific item in it, how can I compute that?
For example if the table looks like
Order# | Item | Total for Entire order
a Apple 50
a Juice 50
a Chicken 50
a Bread 50
b Bread 23
b fish 23
c Chicken 43
c Wine 43
c rice 43
I want get the avg total of all orders that contain Chicken in them at least once? but dont want to count the total of once order twice in my average calculation - Thanks for looking

If one has the new Dynamic Array formulas:
=AVERAGE(INDEX(UNIQUE(FILTER(A2:C10,B2:B10="Chicken")),,3))
If not:
=SUMPRODUCT(((B2:B10="Chicken")*(C2:C10))/(COUNTIFS(A2:A10,A2:A10,B2:B10,"Chicken")+(B2:B10<>"Chicken")))/SUMPRODUCT((B2:B10="Chicken")/(COUNTIFS(A2:A10,A2:A10,B2:B10,"Chicken")+(B2:B10<>"Chicken")))

If you're not opposed to using VBA this function will do what you want.
=Average_Orders_Subset(range, criteria)
I changed this from my original answer to improve the functionality and added comments so you can see what it's doing. With the function you can add as many orders as you like. I have also tested it and it will work if the order letters are changed to numbers i.e. a = 1, b= 2 ect... Hope this helps. Don't hesitate to ask if you have any questions.
If your data starts in cell A1 it works like this:
Example: =Average_Orders_Subset(A1:C13,"Chicken")
With this version you can average for Chicken, rice, bread, anything you want.
In case you don't know here's how to add the code to the workbook.
If you want I can send you a copy of a workbook with it already built in.
Option Explicit
' User Defined Function to Average totals for only orders that contain specific string
' Pass the range and string you are looking for to the function
' Note the criteria string is case senstive so "chicken" is not the same as "Chicken"
'
' Example: =Average_Orders_Subset(A1:C13,"Chicken")
'
Public Function Average_Orders_Subset(rng As Range, criteria As String) As Double
Dim ws As Worksheet
Dim arrData() As Variant
Dim arrOrder() As Variant
Dim i As Long
Dim j As Long
Dim cnt As Long
Dim sum As Double
Dim avg As Double
' The worksheet with data
Set ws = ThisWorkbook.ActiveSheet
' Counter and array to keep track of order letters
cnt = 0
ReDim arrOrder(cnt)
With ws
' Create an array with all the values
arrData = rng.Value2
' Iterate through the array looking for orders with the criteria e.g., "Chicken"
For i = 2 To UBound(arrData)
' If criteria is found
If arrData(i, 2) = criteria Then
If cnt > 0 Then
' If the array of order letters is less than 0
If arrData(i, 1) <> arrOrder(cnt - 1) Then
' Checking if the order letter is already in the array so orders with two Chicken
' or multipe of any criteria don't get double counted
' Add them to the order letter array
arrOrder(cnt) = arrData(i, 1)
cnt = cnt + 1
ReDim Preserve arrOrder(cnt)
End If
ElseIf cnt = 0 Then
' This is to add the first occurence of the critera to the order array
arrOrder(cnt) = arrData(i, 1)
cnt = cnt + 1
ReDim Preserve arrOrder(cnt)
End If
End If
Next i
' Remove the last empty value in the order array
' this is a result of the count expanding the array after the value is added
ReDim Preserve arrOrder(UBound(arrOrder) - 1)
' Reset counter
cnt = 0
sum = 0
' For all the values in the order array
For i = LBound(arrOrder) To UBound(arrOrder)
' For all the values in the range
For j = 2 To UBound(arrData)
' If a row in the range matches the order letter add that value to the sum
If arrData(j, 1) = arrOrder(i) Then
sum = sum + arrData(j, 3)
' Keep track of the number of summed values for an average
cnt = cnt + 1
End If
Next j
Next i
' Calculte the average
avg = (sum / cnt)
' Pass the average back to the formula
Average_Orders_Subset = avg
End With
Set rng = Nothing
Set ws = Nothing
End Function

Related

Split time range in 1 hour intervals

want to split a time range into 1 hour intervals
split the given time range into 1 hour intervals from cell A2 and A3, the time range will be changed a serval time and on a (Macro) click it should split the given time range into 1 hour intervals.
Create an Hourly Sequence
Sub CreateHourlySequence()
Dim ws As Worksheet: Set ws = ActiveSheet ' improve!
Dim dt1: dt1 = ws.Range("A2").Value
Dim dt2: dt2 = ws.Range("A3").Value
Dim dfCell As Range: Set dfCell = ws.Range("C2")
dfCell.Resize(ws.Rows.Count - dfCell.Row + 1).ClearContents
Select Case False
Case IsDate(dt1), IsDate(dt2): Exit Sub
End Select
Dim dtDiff As Long: dtDiff = DateDiff("h", dt1, dt2)
Dim dtStart As Date, dStep As Long
Select Case dtDiff
Case Is > 0: dtStart = dt1: dStep = 1
Case Is < 0: dtStart = dt2: dStep = -1
End Select
Dim rCount As Long: rCount = Abs(dtDiff) + 1
Dim Data() As Date: ReDim Data(1 To rCount, 1 To 1)
Dim d As Long, r As Long
If dStep = 0 Then
Data(1, 1) = dtStart
Else
For d = 0 To dtDiff Step dStep
r = r + 1
Data(r, 1) = DateAdd("h", d, dtStart)
Next d
End If
dfCell.Resize(rCount).Value = Data
End Sub
If you are ok with a non-VBA solution, then you have some options.
Option 1: SEQUENCE
For the Excel version listed here, you could use the SEQUENCE function as suggested by chris neilsen.
Example:
Let's assume that your data starts at A1 like this:
Then, in C2, you could have :
=SEQUENCE((A3-A2)/VALUE("01:00:00")+1,1,A2,VALUE("01:00:00"))
Note that VALUE("01:00:00") represents 1 hour (but you could also use TIME(1,,) as suggested by Mayukh Bhattacharya).
Option 2: Dynamic Array Formula
You have an Excel version listed here, you can use a dynamic array formula .
Example:
Making the same assumptions as option 1, for where the data is, you could use a formula like this one:
=(ROW(INDIRECT("1:"&(A3-A2)/VALUE("01:00:00")+1))-1)*VALUE("01:00:00")+A2
Explanations:
Using the INDIRECT function inside the ROW function is a neat trick to get an array with consecutive values. For instance, INDIRECT("1:9") return the array containing rows 1 to 9 and passing it to ROW will return the array as a column like this {1;2;3;4;5;6;7;8;9} (we get only one element per row).
Since we don't know in advance how many steps we will take we calculate the number of elements using (A3-A2)/VALUE("01:00:00")+1 and concatenate it to "1:" to get the range of size that we need.
When we have the sequential array, we just need to make sure it starts by zero, which is why we remove 1 to all elements of the array like this:
ROW(INDIRECT("1:"&(A3-A2)/VALUE("01:00:00")+1))-1
Finally, we multiply each element of the array by the value corresponding to 1 hour and add the starting point in A2.
Option 3: Old array formula
Same idea as option 3 but using the old array formula explained here. Basically, you'll have to use Ctrl+Shift+Enter.
A Simple Solution given your example (to clear cells it is your job :-)
It would be better to write to an array but as example it should be ok.
Option Explicit
Sub TimeToHour()
Dim startTime As Double, endTime As Double, i As Double, z As Double
startTime = Range("a2")
endTime = Range("a3")
Columns(3).NumberFormat = Range("a2").NumberFormat ' Column C
z = 2
For i = startTime To endTime Step 1 / 24
Cells(z, 3) = i ' write to column c starting in row 2
z = z + 1
Next
End Sub

Assign a variable to cells to compare mutliple numbers

I have a data set where I need to compare the first number in each transect against each other.
For example, in the below data set I need to compare cells D2, D7, D12 and D17 and assign a value based on which one is the smallest number, the next smallest and so on. This will be used to assign the transect numbers in column A.
My issue is that the number of sections (in this example 4) and the number of transects (also 4 in this example) will vary. So the cells I need to compare will change.
I have written the code that calculates the number of transects, which is:
Dim tlength As Worksheet
Dim tb As Long *'tb=transect break*
Sub tlength_start_stop_coords()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("sheet1")
Set tlength = ThisWorkbook.Worksheets("transect lengths") *' assigns the sheet to a variable
for efficient coding*
tb = 0 *'counter to calculate the number of transects*
j = 2 *'counter for row*
Lastrow = Lastrow + 1 *'add a row to last row so that the last row includes a blank line.*
*'the following for loop is used to calculate the number of transects*
For i = 2 To Lastrow
If tlength.Range("D" & i) = vbNullString Then
If tlength.Range("D" & i - 1) <> vbNullString Then
tb = tb + 1 *'updates the counter*
End If
End If
Next i
tbtotal = tb *'stores the total counter in variable tbtotal for later use*
I think I may need to use a loop. But I am stuck trying to figure out how to manage the unknown number of comparisons in changing cell locations.
The desired result is in the below screenshot of the expected outcome, with results in column A. To begin with, I only need to get the number for the first row of each transect. Once I have that, I can copy using xldown.
Expected outcome:
Another possible data set and outcome expected might be:
enter image description here
with an expected outcome of:
enter image description here
Worked for me using your second set of sample data:
Sub Tester()
Dim tlength As Worksheet, i As Long, tNum As Long, v, vPrev, arr
Dim col As New Collection, e, c As Range, rng As Range
Set tlength = ThisWorkbook.Worksheets("transect lengths")
'collect all the Section 1 Latitudes and row numbers
For i = 2 To tlength.Cells(Rows.Count, "B").End(xlUp).Row
If tlength.Cells(i, "B") = 1 Then
col.Add Array(i, tlength.Cells(i, "D").Value) 'store start row and first Latitude
End If
Next i
SortCollection col, 2 'sort collection by second element in each array
tNum = 0
'loop over the sorted collection and assign the order of the transects
For Each e In col
tNum = tNum + 1
Set c = tlength.Cells(e(0), "B")
'following assumes all transects have at least 2 sections...
tlength.Range(c, c.End(xlDown)).Offset(0, -1).Value = tNum
Next e
End Sub
'Sort a collection of 0-based arrays in ascending order by the n'th element of each array
'Adapted from https://stackoverflow.com/a/3588073/478884
Sub SortCollection(col As Collection, n As Long)
Dim i As Long, j As Long, vTemp As Variant
For i = 1 To col.Count - 1 'Two loops to bubble sort
For j = i + 1 To col.Count
If col(i)(n - 1) < col(j)(n - 1) Then 'change to > for ascending sort
vTemp = col(j) 'store the lesser item
col.Remove j 'remove the lesser item
col.Add Item:=vTemp, before:=i 're-add the lesser item before the greater Item
End If
Next j
Next i
End Sub

VBA to shift inventory rows up when items are deleted?

Last summer, I made a pretty basic VBA for an inventory sheet. Column A lists item name, and column B, C, and D are additional info for each item listed. The inventory is two-columned (A2:D28 and G2:J28). The VBA I made makes it so that if I delete the item entry in cell A4, the info in cells B4:D4 clears automatically with it.
The question is: I've been struggling to find a way to make the cells shift up a row when the row above it is cleared, to prevent the list from accumulating empty rows as inventory items are deleted. Most examples I found online were to delete those empty cells, whereas I'd rather just clear them and keep my formatting intact.
Is there a way to shift specific cells up like that? And, it would be lovely if there was a way to shift items from the top of the second table (G2:J2) down and over to the bottom of the first table, into A28:D28.
Any help would be greatly appreciated, or even a thumb towards a relevant tutorial. Thank you!
This code is a little tricky because of the two columns you have on your screen. My code below reads the two columns into a single one, sorts blank rows to the bottom and then splits the resulting single column back into two. All of this is done without touching the cells themselves. Therefore formatting stays in place.
Option Explicit
Enum Nsp ' Table specs
' 023
' These enumerations define your table. Modify to suit
NspAnchorClm = 1 ' 1 = column A
NspTblClmCount = 4 ' number of Table Columns
NpsSpaceClmCount = 2 ' number of blank sheet columns between List Columns
NspListClmCount = 2 ' number of List Columns
NspFirstRow = 2
NspNumRows = 10 ' number of rows per List Column
End Enum
Sub ResetList()
' 023
Dim Clm() As Variant ' First sheet column of each list column
Dim ArrIn As Variant ' Input data array
Dim Cin As Long ' input column counter
Dim Rin As Long ' input row counter
Dim ArrOut As Variant ' Output data array
Dim Rout As Long ' output row counter
Dim Rng As Range ' the sheet range of varying dimension
Dim Tmp As Variant ' intermediate memory
Dim L As Integer ' List column counter
Dim C As Long ' column counter
Dim R As Long ' row counter
Clm = Array(NspAnchorClm, NspAnchorClm + NspTblClmCount + NpsSpaceClmCount)
Tmp = (NspTblClmCount * NspListClmCount) + (NpsSpaceClmCount * (NpsSpaceClmCount - 1))
Set Rng = Range(Cells(NspFirstRow, Clm(0)), _
Cells(NspFirstRow + NspNumRows - 1, Tmp))
' read all of the list into an array
ArrIn = Rng.Value
' define a single list column array
ReDim ArrOut(1 To NspNumRows * NspListClmCount, 1 To NspTblClmCount)
' transfer the data to a single list column
For L = 1 To NspListClmCount
For R = 1 To NspNumRows
Rout = (L - 1) * NspNumRows + R
For C = 1 To NspTblClmCount
Cin = Clm(L - 1) + C - 1
ArrOut(Rout, C) = ArrIn(R, Cin)
Next C
Next R
Next L
' ArrIn is cleared and re-purposed to take data from ArrOut
ReDim ArrIn(1 To UBound(ArrOut), 1 To UBound(ArrOut, 2))
Rin = 0
For Rout = 1 To UBound(ArrOut)
' skip rows where the first column is blank
If Len(ArrOut(Rout, 1)) Then
Rin = Rin + 1
For C = 1 To UBound(ArrOut, 2)
ArrIn(Rin, C) = ArrOut(Rout, C)
Next C
End If
Next Rout
' assign NspNumRows high sections of ArrIn to ArrOut
For L = 1 To NspListClmCount
ReDim ArrOut(1 To NspNumRows, 1 To NspTblClmCount)
For R = 1 To NspNumRows
For C = 1 To NspTblClmCount
ArrOut(R, C) = ArrIn(((L - 1) * NspNumRows) + R, C)
Next C
Next R
Set Rng = Cells(NspFirstRow, Clm(L - 1)).Resize(NspNumRows, NspTblClmCount)
Rng.Value = ArrOut
Next L
End Sub
I'm afraid this code will make the code you already have obsolete. If the first cell in a row is empty any content in the others will be omitted, just like your own code does.
Please pay attention to the enumeration at the top of the code. It works like a switchboard where you can enter all parameters. You can modify them as you wish. For example my code has NspNumRows = 10. Your sheet has 27 data rows per column. You will need to change that number. Just to help you find your way:-
The "AnchorColumn" is the first sheet column of the first list column. All other columns are counted from there. It need not be column A. You could leave column A blank and anchor your list in Column B (=2).
"TableColumns" are the columns that repeat in each "ListColumn". You can have more than 4 or fewer.
A "ListColumn" consists of several "TableColumns". You can have more than 2.
"SpaceColumns" are blank sheet columns inserted between "ListColumns".
NspFirstRow specifies the first data row. Above it are captions or other data which this program doesn't touch on. You could reserve the first 10 rows for something else and start your list in row 11.
By setting these 6 enumerations you can create lists of 2 or more List Columns, anywhere on the worksheets, with any number of data rows. Not all of this has been tested exhaustively. When you delete a row anywhere (in fact only the first cell of that row) the list is rewritten to move the blank row to the bottom of the last list column.

Copy Two dimensional array to another array based on criteria in VBA

I would like to copy data from one sheet to another.
I put the range that I want to copy into an array (LookupSource) because it's faster to work on arrays than looping through cells.
After filling my two dimensional array (LookupSource), I would like to keep only some records based on critieria (Column A = 10000), so I am trying to copy from LookupSource, the rows that fetch this criteria to the two dimensional array (DataToCopy) which will be copied to the destination sheet.
My problem is that I am not able to do that because as it seems I am not able to make a dynamic resize for the first dimension (rows) of the second array (DataToCopy).
Any Idea how to fill DataToCopy from LookupSource based on my condition ?
The error "index out of range" that I am getting is at the Line : ReDim Preserve DataToCopy(1 to j, 1 to 6)
not at first time, but on second time that I enter the For loop after the Next I
I suppose it's because the J is variable and I am not allowed to change the first dimension of the array.
How to deal with that ?
Any better Idea from what I am doing ?
to give you an example here is a small part of the sheet that I want to copy (I took only 8 rows, but in real there thousands). I want to copy only the rows that have 10000 in column A.
Here is my code
Dim LookupSource as Variant
Dim DataToCopy() As Variant
Dim i As Long
Dim j As Long
With MySheet
'MyRange is a defined name that reprensent column A, B, C, D, E, F
LookupSource = .Range(.Range("MyRange")(1, 1), .Range("MyRange")(8, 6)).Value2
j = 1
For i = LBound(LookupSource) To UBound(LookupSource)
If LookupSource(i, 1) = 10073 Then
ReDim Preserve DataToCopy(1 to j, 1 to 6)
DataToCopy(j, 1) = LookupSource(i, 1)
DataToCopy(j, 2) = LookupSource(i, 2)
DataToCopy(j, 3) = LookupSource(i, 3)
DataToCopy(j, 4) = LookupSource(i, 4)
DataToCopy(j, 5) = LookupSource(i, 5)
DataToCopy(j, 6) = LookupSource(i, 6)
j = j + 1
End If
Next i
end with
How to overcome the restrictions of ReDim Preserve in multidimensional arrays
As mentioned by #ScottCraner, a ReDim Preserve can change only the last dimension of a given (datafield) array.
Therefore trying to resize a 2-dimensional array's first dimension (="rows") will fail.
However you can overcome this inconvenience applying the relatively unknown filtering capability of Application.Index() (c.f. section [2]) and profit from the additional bonus of less loops.
Further reading: see Some pecularities of the Application.Index() function
Sub GetRowsEqual10000()
With Sheet1
Dim lastRow As Long: lastRow = .Range("A" & .Rows.Count).End(xlUp).Row
Dim rng As Range: Set rng = .Range("A2:F" & lastRow)
End With
'[1] get data
Dim data: data = rng
'[2] rearrange data via Application.Index() instead ReDim Preserve plus loops
data = Application.Index(data, ValidRows(data, Condition:=10000), Array(1, 2, 3, 4, 5, 6))
End Sub
Help function ValidRows()
Function ValidRows(arr, Condition) As Variant
'Purpose: a) check condition (e.g. values equalling 10000) and b) get valid row numbers c) in a 2-dim vertical array
ReDim tmp(1 To UBound(arr)) ' provide for 1-based 2-dim array
Dim i As Long, ii As Long
For i = 1 To UBound(arr) ' loop through 1st "column"
If arr(i, 1) = Condition Then ' a) check condition
ii = ii + 1: tmp(ii) = i ' b) collect valid row numbers
End If
Next i
ReDim Preserve tmp(1 To ii) ' resize tmp array (here the 1st dimension is also the last one:)
ValidRows = Application.Transpose(tmp) ' c) return transposed result as 2-dim array
End Function
Edit due to comment (2020-04-22)
Short hints to the most frequent use of Application.Index():
Frequently the Application.Index() function is used to
get an entire row or column array out of a 2-dim array without need to loop.
Accessing your 1-based 2-dimensional datafield array like that requires to
indicate a single row or column number and
to set the neighbour argument column or row number to 0 (zero), respectively which might result in e.g.
Dim horizontal, vertical, RowNumber As Long, ColumnNumber As Long
RowNumber = 17: ColumnNumber = 4
horizontal = Application.Index(data, RowNumber, 0)
vertical = Application.Index(data, 0, ColumnNumber)
(Addressing a single array element will be done directly, however via data(i,j)
instead of a theoretical Application.Index(data, i, j))
How to use Application.Index() for restructuring/filtering purposes:
In order to profit from the advanced possibilities of Application.Index() you
need to pass not only the array name (e.g. data), but the row|column arguments as Arrays, e.g.
data = Application.Index(data, Application.Transpose(Array(15,8,10)), Array(1, 2, 3, 4, 5, 6))
Note that the rows parameter becomes a "vertical" 2-dim array by transposition, where Array(15,8,10)
would even change the existing row order
(in the example code above this is done in the last code line within the ValidRows() function).
The columns argument Array(1,2,3,4,5,6) on the other hand remains "flat" or "horizontal" and
allows to get all existing column values as they are.
So you eventually you are receiving any data elements within the given element indices
(think them as coordinates in a graphic).
Range Lookup Function
The Code
Option Explicit
'START ****************************************************************** START'
' Purpose: Filters a range by a value in a column and returns the result '
' in an array ready to be copied to a worksheet. '
'******************************************************************************'
Function RangeLookup(LookUpValue As Variant, LookupRange As Range, _
Optional LookupColumn As Long = 1) As Variant
Dim LookUpArray As Variant ' LookUp Array
Dim DataToCopy As Variant ' DataToCopy (RangeLookup) Array
Dim countMatch As Long ' DataToCopy (RangeLookUp) Rows Counter
Dim r As Long, c As Long ' Row and Column Counters
' Check the arguments.
Select Case VarType(LookUpValue)
Case 2 To 8, 11, 17
Case Else: Exit Function
End Select
If LookupRange Is Nothing Then Exit Function
If LookupColumn < 1 Or LookupColumn > LookupRange.Columns.Count _
Then Exit Function
' Copy values of Lookup Range to Lookup Array.
LookUpArray = LookupRange
' Task: Count the number of values containing LookUp Value
' in LookUp Column of LookUp Array which will be
' the number of rows in DataToCopy Array.
' The number of columns in both arrays will be the same.
' Either:
' Count the number of values containing LookUp Value.
countMatch = Application.WorksheetFunction _
.CountIf(LookupRange.Columns(LookupColumn), LookUpValue)
' Although the previous looks more efficient, it should be tested.
' ' Or:
' ' Loop through rows of LookUpArray.
' For r = 1 To UBound(LookUpArray)
' ' Check if the value in current row in LookUp Column
' ' is equal to LookUp Value.
' If LookUpArray(r, LookupColumn) = LookUpValue Then
' ' Increase DataCopy Rows Counter.
' countMatch = countMatch + 1
' End If
' Next r
' Check if no match was found.
If countMatch = 0 Then Exit Function
' Task: Write the matching rows in LookUp Array to DataToCopy Array.
' Resize DataToCopy Array to DataToCopy Rows counted in the previous
' For Next loop and the number of columns in Lookup Array.
ReDim DataToCopy(1 To countMatch, 1 To UBound(LookUpArray, 2))
' Reset DataToCopy Rows Counter.
countMatch = 0
' Loop through rows of LookUp Array.
For r = 1 To UBound(LookUpArray)
' Check if the value in current row in LookUp Column
' is equal to LookUp Value.
If LookUpArray(r, LookupColumn) = LookUpValue Then
' Increase DataCopy Rows Counter.
countMatch = countMatch + 1
' Loop through columns of LookUp (DataToCopy) Array.
For c = 1 To UBound(LookUpArray, 2)
' Write the current value of LookUp Array to DataToCopy Array.
DataToCopy(countMatch, c) = LookUpArray(r, c)
Next c
End If
Next r
' Write values from DataToCopy Array to RangeLookup Array.
RangeLookup = DataToCopy
End Function
'END ********************************************************************** END'
You should use it e.g. like this:
Sub TryRangeLookup()
Dim LookupRange As Range
Dim DataToCopy As Variant
With MySheet
'MyRange is a defined name that reprensent column A, B, C, D, E, F
Set LookupRange = .Range(.Range("MyRange")(1, 1), _
.Range("MyRange")(8, 6)).Value2
End With
RangeLookUp 10073, DataCopy
If Not IsArray(DataToCopy) Then
MsgBox "No data found.": Exit Sub ' or whatever...
Endif
' Continue with code...
End Sub

How to check if a string exists in a column in excel where cells contain strings separated by comma

Please click on this link for the image of the excel sheet containing the data:
http://i.stack.imgur.com/Dl1YQ.gif
I have a list of task codes in column A.
During each task I will gain a certain competencies. Each competency listed in column C or E is gained during the tasks listed in columns D and F respectively.
Now I need a formula to tell me on column B (COMPETENCIES), which of the competencies are gained during each task of column A. For example for Task A2 (MSC) I expect to see "Tech1,Tech2,Tech3,Tech4,PS1,PS2,PS3" in column B (B2).
I suppose I should treat task codes in column A as strings that should be looked for in the cell contents of columns D and F and when found in any cell of those columns, the corresponding competency should be copied from the same row on the column to the left of the cell, into column B. And then all these entries should be separated by commas in each cell of column B (if there is more than one competency met during task A2).
Can you help me please?
Many Thanks,
Hamid
I agree with the comments: this is a task for VBA.
I typed your GIF into a worksheet. I have made no attempt to fix what I believe are errors. For example, Column A contains "SEMS" but column D contains "SMES".
Step 1 of the routine below is to work down columns C and D then columns E and F and accumulates the data in an array of structures. The objective is to reverse the relationships to give:
MSC Tech1 Tech2 ...
ATT Tech1 Tech2 ...
: :
The result is them placed in column B.
The first step is quite complicated. I hope I have included enough comments for you to understand my code. Work through it slowly and come back with questions is necessary.
Option Explicit
' VBA as intrinsic data types : string, long, double, etc.
' You can declare an array of longs, say.
' The size of an array can be fixed when it is declared:
' Dim A(1 To 5) As Long
' or it can be declared as dynamic and then resized as necessary:
' Dim A() As Long
' ReDim A(1 to 5) ' Initialise A with five entries
' ReDim Preserve A(1 to 10) ' Preserve the first five entries in A
' ' and add another 5.
'
' Sometimes a more complex structure is required. For this problem we need
' to build a list of Tasks with a list of Competencies against each Task.
' VBA allows us to to define the necessary structure as a "User Type"
' Define a user type consisting of a Task name and an array of Competencies
Type typTaskComp
Task As String
Comp() As String
End Type
' Declare array in which Tasks and Competencies are
' accumulated as a dynamic array of type typTaskComp.
Dim TaskComp() As typTaskComp
Dim InxTaskCrntMax As Long
Sub MatchTaskToCompetencies()
Dim CompListCrnt As String
Dim InxCompCrnt As Long ' Index for Competencies for a Task
Dim InxTaskCrnt As Long ' Index for Tasks
Dim RowCrnt As Long
Dim TaskCrnt As String
ReDim TaskComp(1 To 10) ' Initialise TaskComp for 10 Tasks
InxTaskCrntMax = 0 ' The last currently used row in TaskComp. That
' is, no rows are currently used.
' Load array TaskComp() from the sheet
Call DecodeCompencyTask("Sheet1", 3, 4)
Call DecodeCompencyTask("Sheet1", 5, 6)
' The format and contents of TaskComp is now:
' Competency ...
' Task 1 2 3 4 5 ...
' 1 MSC Tech1 Tech2 Tech3 Tech4 PS1
' 2 ATT Tech1 Tech2 Tech3 Tech4 PS1
' 3 PLCY Tech1 Tech2 Tech4 Tech5 Tech6
' : :
' Display contents of TaskComp() to Immediate window
For InxTaskCrnt = 1 To InxTaskCrntMax
Debug.Print Left(TaskComp(InxTaskCrnt).Task & Space(5), 6);
For InxCompCrnt = 1 To UBound(TaskComp(InxTaskCrnt).Comp)
If TaskComp(InxTaskCrnt).Comp(InxCompCrnt) = "" Then
Exit For
End If
Debug.Print Left(TaskComp(InxTaskCrnt).Comp(InxCompCrnt) & Space(5), 6);
Next
Debug.Print
Next
' Now place lists of Competencies in Column 2 against appropriate Task
RowCrnt = 2
With Worksheets("Sheet1")
TaskCrnt = .Cells(RowCrnt, 1).Value
Do While TaskCrnt <> ""
For InxTaskCrnt = 1 To InxTaskCrntMax
If TaskCrnt = TaskComp(InxTaskCrnt).Task Then
' Have found row in TaskComp that matches this row in worksheet
' Merge list of Competencies into a list separated by commas
CompListCrnt = Join(TaskComp(InxTaskCrnt).Comp, ",")
' Empty entries at the end of TaskComp(InxTaskCrnt).Comp will
' result in trailing commas. Remove them.
Do While Right(CompListCrnt, 1) = ","
CompListCrnt = Mid(CompListCrnt, 1, Len(CompListCrnt) - 1)
Loop
' and place in column 2
.Cells(RowCrnt, 2).Value = CompListCrnt
Exit For
End If
Next
RowCrnt = RowCrnt + 1
TaskCrnt = .Cells(RowCrnt, 1).Value
Loop
End With
End Sub
Sub DecodeCompencyTask(WShtName As String, ColComp As Long, ColTask As Long)
' Sheet WShtName contains two columns numbered ColComp and ColTask, Column
' ColComp contains one Competency per cell. Column ColTask holds a comma
' separated list of Tasks per cell. For each row, the Competency is gained
' by performing any of the Tasks.
' Scan the two columns. If a Task is missing from TaskComp() prepare a row
' for it. Add the Competency to the new or existing row for the Task.
Dim CompCrnt As String
Dim Found As Boolean
Dim InxCompCrnt As Long ' Index for Competencies for a Task
Dim InxTaskCrnt As Long ' Index for Tasks
Dim RowCrnt As Long
Dim TaskCrnt As Variant
Dim TaskList() As String
With Worksheets(WShtName)
RowCrnt = 2
Do While .Cells(RowCrnt, ColComp).Value <> ""
CompCrnt = .Cells(RowCrnt, ColComp).Value ' Extract Competency
' Remove any spaces from Task List and then split it
' so there is one Task per entry in TaskList.
TaskList = Split(Replace(.Cells(RowCrnt, ColTask).Value, " ", ""), ",")
' Process each task in TaskList
For Each TaskCrnt In TaskList
Found = False
' Look for current Task in existing rows
For InxTaskCrnt = 1 To InxTaskCrntMax
If TaskComp(InxTaskCrnt).Task = TaskCrnt Then
Found = True
Exit For
End If
Next
If Not Found Then
' New Task found. Prepare new row with Task but no
' Competencies
InxTaskCrntMax = InxTaskCrntMax + 1
If InxTaskCrntMax > UBound(TaskComp) Then
' No free rows in TaskComp. Add some more rows
ReDim Preserve TaskComp(1 To UBound(TaskComp) + 10)
End If
InxTaskCrnt = InxTaskCrntMax
TaskComp(InxTaskCrnt).Task = TaskCrnt
ReDim TaskComp(InxTaskCrnt).Comp(1 To 5)
' Rely on array entries being initialised to ""
End If
Found = False
' Look for an empty Competency slot in current row of TaskComp
For InxCompCrnt = 1 To UBound(TaskComp(InxTaskCrnt).Comp)
If TaskComp(InxTaskCrnt).Comp(InxCompCrnt) = "" Then
Found = True
Exit For
End If
Next
If Not Found Then
' Row is full. Add some extra entries and set InxCompCrnt to
' first of these new entries.
InxCompCrnt = 1 + UBound(TaskComp(InxTaskCrnt).Comp)
ReDim Preserve TaskComp(InxTaskCrnt).Comp(1 _
To UBound(TaskComp(InxCompCrnt).Comp) + 5)
End If
TaskComp(InxTaskCrnt).Comp(InxCompCrnt) = CompCrnt
InxCompCrnt = InxCompCrnt + 1
Next
RowCrnt = RowCrnt + 1
Loop
End With
End Sub

Resources