Wrap rows that have duplicates - excel

I've got data that looks like this:
BOB | 4
BOB | 3
BOB | 7
MARY | 1
JOE | 2
JOE | 1
MIKE | 6
I want to end up with data that looks like this:
BOB | 4 | 3 | 7
MARY | 1 | |
JOE | 2 | 1 |
MIKE | 6 | |
The problem is, how do I account for the variable number of times a name shows up?

I came up with the following code. It feels like it could be cleaner.
This will work for any selected block of data on your sheet (assuming it is pre-sorted). It outputs on the same sheet in the same area.
Sub WrapDuplicates()
Dim data(), i As Long, startCell As Range, rwCnt As Long, col As Long
data = Selection //pull selected data into an array
Set startCell = Selection.Cells(1, 1) //Get reference to write results to
Selection.ClearContents //remove original data
startCell = data(1, 1) //Output first name
startCell.Offset(0, 1) = data(1, 2) //Output first value
rwCnt = 0
col = 2
For i = 2 To UBound(data) //Loop through array and check if name is same or not and output accordingly
If data(i, 1) = data(i - 1, 1) Then
startCell.Offset(rwCnt, col) = data(i, 2)
col = col + 1
Else
rwCnt = rwCnt + 1
col = 2
startCell.Offset(rwCnt, 0) = data(i, 1)
startCell.Offset(rwCnt, 1) = data(i, 2)
End If
Next i
End Sub

I'm assuming you want to do this in code based on the excel-vba tag in your post.
I'm also assuming the data is sorted by name, or you are OK with sorting it by name before the code executes.
Source is in sheet 1, target is in sheet 2. Code is in Excel VBA. I tested with your sample data, dropping this subroutine in the ThisWorkbook section of the Excel codebehind and pressing play.
The target header gets rewritten every time, which isn't ideal from a performance perspective, but I don't think is a problem otherwise. You could wrap it in an if statement that checks the target column index = 2 if it becomes a problem.
Sub ColumnsToRows()
Dim rowHeading
Dim previousRowHeading
Dim sourceRowIndex
Dim targetRowIndex
Dim targetColumnIndex
sourceRowIndex = 1
targetRowIndex = 1
targetColumnIndex = 2
rowHeading = Sheet1.Cells(sourceRowIndex, 1)
previousRowHeading = rowHeading
While Not rowHeading = ""
If Not previousRowHeading = rowHeading Then
targetRowIndex = targetRowIndex + 1
targetColumnIndex = 2
End If
Sheet2.Cells(targetRowIndex, 1) = rowHeading
Sheet2.Cells(targetRowIndex, targetColumnIndex) = Sheet1.Cells(sourceRowIndex, 2)
previousRowHeading = rowHeading
sourceRowIndex = sourceRowIndex + 1
targetColumnIndex = targetColumnIndex + 1
rowHeading = Sheet1.Cells(sourceRowIndex, 1)
Wend
End Sub
I'm a developer, not an Excel guru. There may be some Excel function, pivot table, or some other Excel magic that does this for you automatically.

Related

In excel how to make union mapping between two columns

I don't know the term to describe the situation so just make it up
basically for example there are two columns
Col A Col B
----------------------
| date 1 | 2020-02-03|
| date 2 | 2020-03-12|
| date 3 | 2020-04-25|
======================
I'd like to have a function to generate following results
Col C
----------------------
| date 1 = 2020-02-03|
| date 1 = 2020-03-12|
| date 1 = 2020-04-25|
| date 2 = 2020-02-03|
| date 2 = 2020-03-12|
| date 2 = 2020-04-25|
| date 3 = 2020-02-03|
| date 3 = 2020-03-12|
| date 3 = 2020-04-25|
======================
it's like concat union of each row from both column values but allow to add extra string (= for example).
Place your first table as follows, i.e. add column headers. (this is my existing code, which requires column header. Too lazy to modify it, so, please follow)
Change the value of paraVal in below Sub to be the range of your first table (including the headers) and run it. The combination will be generated below your first table (so, make sure there's sufficient space for the result). Combine the resulted columns by your own way (e.g. TEXTJOIN, CONCAT...)
Sub CombinationTable()
Dim paraVal As Range
Dim paraInfo() As Long
Dim rowTtl As Long
Dim colIdx As Long
Dim repIdx As Long
Dim colIdxG As Long
Dim rowIdxG As Long
Dim rowStartG As Long
Dim rowEndG As Long
Dim colCell1 As Range
Dim colCellN As Range
Dim repeat As Range
Set paraVal = Range("F4", "G7")
On Error GoTo 0
With paraVal
rowTtl = 1
ReDim paraInfo(1 To .Columns.Count)
For colIdx = 1 To .Columns.Count
If IsEmpty(.Cells(.Rows.Count - 1, colIdx)) Then ' .rows.count-1 = 1st value
paraInfo(colIdx) = 0
Else
Set colCellN = .Cells(.Rows.Count, colIdx)
Set colCell1 = colCellN.End(xlUp)
paraInfo(colIdx) = colCellN.Row - colCell1.Row ' no +1 bcoz last row is header, not value
rowTtl = rowTtl * paraInfo(colIdx)
End If
Next colIdx
rowStartG = .Row + .Rows.Count
rowEndG = rowStartG + rowTtl - 1
For colIdx = 1 To .Columns.Count
If paraInfo(colIdx) > 0 Then
rowTtl = rowTtl / paraInfo(colIdx)
rowIdxG = rowStartG
colIdxG = .Columns(colIdx).Column
Set colCellN = .Columns(colIdx).Cells(.Columns(colIdx).Rows.Count).Offset(-paraInfo(colIdx)).Resize(paraInfo(colIdx))
For Each colCell1 In colCellN.Cells
ActiveSheet.Range(ActiveSheet.Cells(rowIdxG, colIdxG), ActiveSheet.Cells(rowIdxG + rowTtl - 1, colIdxG)).Value = colCell1.Value
rowIdxG = rowIdxG + rowTtl
Next colCell1
Set repeat = ActiveSheet.Range(ActiveSheet.Cells(rowStartG, colIdxG), ActiveSheet.Cells(rowIdxG - 1, colIdxG))
If colIdx > 1 Then
repeat.Copy Destination:=ActiveSheet.Range(ActiveSheet.Cells(rowIdxG, colIdxG), ActiveSheet.Cells(rowEndG, colIdxG))
End If
End If
Next colIdx
End With
End Sub

If statement with multiple conditions, how to fix

I want to be able to parse through a worksheet and remove certain rows based on criteria "Logged Off" being in one of the cells. The trick is, it can't be every instance of it, just the ones where the next row shows one of the parameters of the 'status' array. The approximate size of the worksheet is 4 columns and roughly 10000~ rows.
Dim firstRow As Long
Dim nextRow As Long
Dim currentDate(1 To 5) As String
Dim totalDelete As Long
Dim p As Integer
Dim i As Integer
Dim status(1 To 5) As String
status(1) = "Available"
status(2) = "Email"
status(3) = "Available, No ACD"
status(4) = "Aux, Technical Issues"
status(5) = "Aux, Client Callback"
currentDate(1) = Sheets("Cover").Range("E12")
currentDate(2) = Sheets("Cover").Range("F12")
currentDate(3) = Sheets("Cover").Range("G12")
currentDate(4) = Sheets("Cover").Range("H12")
currentDate(5) = Sheets("Cover").Range("I12")
firstRow = 2
nextRow = 3
totalDelete = 0
For i = 1 To 5
For p = 1 To 5
Do While firstRow <= 10000
If Cells(firstRow, "C") = "Logged Off" And Cells(nextRow, "C") = status(p) And Cells(nextRow, "B") = currentDate(i) Then
Rows(firstRow).Delete
totalDelete = totalDelete + 1
Else
firstRow = firstRow + 1
nextRow = nextRow + 1
End If
Loop
Debug.Print currentDate(p)
Debug.Print status(p)
Next p
Next i
Debug.Print totalDelete
Now what I'm expecting is it to loop through about 10000 rows and check for what I described above. It goes through a couple loops to check for all possible dates and status' in the two arrays that I've had. The debugs in the code are just me checking to see if the status and currentDate are outputting correctly, and they are. That leads me to believe something is probably wrong with my IF statement. However, I'm not the most adept at this, so I'm just failing to see where I'm going wrong.
A | B | C | D
1 data | 3/25/2019 | Logged Off | data
2 data | 3/25/2019 | Logged Off | data
3 data | 3/25/2019 | email | data
4 data | 3/25/2019 | email | data
So after running the code, I would expect it to remove row 2 at the very least.
There's a couple of issues in your code.
1) When you plan to delete rows in a loop, work backwards.
Imagine that you hit row 3 and you have to delete it (firstRow = 3). You delete it and now Row 4 is Row 3 and you iterate firstRow to 4. Essentially the row that WAS row 4 (and is now Row 3) is skipped and isn't checked.
Instead
FirstRow = 10000
Do While firstRow >= 2
and decrement firstrow in each loop:
firstRow = firstRow - 1
to insure you don't pull the rug out from under yourself. That may or may not fix the issue you are seeing, but it's a definite bug in your code.
2) your loops inside loops
That's 5 x 5 x 10000 loops or 250000 loops. That's pretty aggressive. Instead just loop your 10000 times and test like:
If Cells(firstRow, "C") = "Logged Off" And inStr(1, Join(status, "|"), Cells(firstRow+ 1, "C").value, 1) And InStr(1, Join(currentDate, "|"), Cells(firstRow + 1, "B"), 1) Then
You can just that Join() function to turn the array into a single string delimited by | between each element. Instr() then tests to see if your cell value is IN that String. We set the last parameter of Instr() to 1 so that it's not case sensitive. 10000 loops will be much faster.
3) You don't need the nextRow variable (although this is just nit-picky so you can ignore if you are married to that thing.)
Instead use Cells(firstRow + 1, "C") or Cells(firstRow, "C").Offset(,1) to do that check. Less variables to increment and track this way.
Here's the rewrite:
Dim firstRow As Long
Dim currentDate(1 To 5) As String
Dim totalDelete As Long
Dim status(1 To 5) As String
status(1) = "Available"
status(2) = "Email"
status(3) = "Available, No ACD"
status(4) = "Aux, Technical Issues"
status(5) = "Aux, Client Callback"
currentDate(1) = Sheets("Cover").Range("E12")
currentDate(2) = Sheets("Cover").Range("F12")
currentDate(3) = Sheets("Cover").Range("G12")
currentDate(4) = Sheets("Cover").Range("H12")
currentDate(5) = Sheets("Cover").Range("I12")
firstRow = 10000
totalDelete = 0
Do While firstRow >= 2
If Cells(firstRow, "C") = "Logged Off" And inStr(1, Join(status, "|"), Cells(firstRow+ 1, "C").value, 1) And InStr(1, Join(currentDate, "|"), Cells(firstRow + 1, "B"), 1) Then
Rows(firstRow).Delete
totalDelete = totalDelete + 1
End If
firstRow = firstRow - 1
Loop
Debug.Print totalDelete

resorting table using array

am trying to resort the data using Code consider the data shape like this :
Empid| 1/01/2019|2/01/2019 | 3/01/2019
-------------------------------------------
1 | A | B | A
2 | B | A | B
3 | B | C | C
4 | A | A | A
and the goal shape like this :
Empid | Date | Shift
---------------------
1 |1/01/2019 | A
1 |2/01/2019 | B
1 |3/01/2019 | A
2 |1/01/2019 | B
2 |2/01/2019 | A
2 |3/01/2019 | B
3 |1/01/2019 | B
3 |2/01/2019 | C
3 |3/01/2019 | C
4 |1/01/2019 | A
4 |2/01/2019 | A
4 |3/01/2019 | A
i used this code and reached to this shape using the code :
Empid | Shift
---------------------
1 |A
1 |B
1 |A
2 |B
2 |A
2 |B
3 |B
3 |C
3 |C
4 |A
4 |A
4 |A
this is the vba code :
Sub TransposeData()
Const FirstDataRow As Long = 2 ' presuming row 1 has headers
Const YearColumn As String = "A" ' change as applicable
Dim Rng As Range
Dim Arr As Variant, Pos As Variant
Dim Rl As Long, Cl As Long
Dim R As Long, C As Long
Dim i As Long
With ActiveSheet
Cl = .UsedRange.Columns.Count - .UsedRange.Column + 1
Rl = .Cells(.Rows.Count, Columns(YearColumn).Column).End(xlUp).Row
Set Rng = Range(.Cells(FirstDataRow, YearColumn), .Cells(Rl, Cl))
End With
Arr = Rng.Value
ReDim Pos(1 To (UBound(Arr) * UBound(Arr, 2)), 1 To 2)
For R = 1 To UBound(Arr)
For C = 2 To UBound(Arr, 2)
i = i + 1
Pos(i, 1) = Arr(R, 1)
Pos(i, 2) = Arr(R, C)
Next C
Next R
R = Rl + 5 ' write 5 rows below existing data
Set Rng = ActiveSheet.Cells(R, YearColumn).Resize(i, 2)
Rng.Value = Pos
End Sub
Use Power Query (aka Get & Transform in Excel 2016+).
Select the first column and UNpivot the other columns.
Rename the resultant Date column (which will be named Attributes by the GUI), and the Shift column (which will be named Value by the GUI).
If you want to do this in VBA, record a macro while running PQ
With a single cell selected in your table, select Get & Transform from Table/Range
Power Query will open. Ensure you have selected the first column. Then, from Transform, select the dropdown next to the Unpivot button. From that dropdown, select unpivot other columns.
After selecting that, you will see that you need to rename columns 2 and 3
After that, select one of the Close options from the File menu, and load the results to either the same sheet or another sheet.
Now you can rerun the query if your data changes.
And, as I wrote above, if you need to do this using VBA, just record a macro while you go through the steps.
I also suggest you search SO for unpivot and you'll get a lot of information.
Array Approach
Option Explicit
Public Sub Rearrange()
Dim t#: t = timer ' stop watch
Dim ws As Worksheet ' worksheet object
Set ws = ThisWorkbook.Worksheets("Sheet3") ' << change to sheet name
Const STARTCOL = "A" ' << change to your needs
' [1] get last row in column A
Dim r&, c& ' used rows/cols (assuming no blanks)
r = ws.Range(STARTCOL & ws.Rows.count).End(xlUp).Row
c = ws.Columns(STARTCOL).End(xlToRight).Column - ws.Columns(STARTCOL).Column
' [2] get values to 1-based 2-dim variant arrays
Dim tmp, tgt
tmp = ws.Range(ws.Cells(1, STARTCOL), ws.Cells(r, c + 1)).Value2
ReDim tgt(1 To c * (UBound(tmp) - 1) + 1, 1 To c) ' resize target array
' [3] rearrange data in target array
Dim i&, ii&, j&
For i = 2 To UBound(tmp)
For j = 2 To UBound(tmp, 2) ' get row data
ii = (i - 1) * c + j - c ' calculate new row index
tgt(ii, 1) = tmp(i, 1) ' get ID
tgt(ii, 2) = tmp(1, j) ' get date
tgt(ii, 3) = tmp(i, j) ' get inditgtidual column data
Next j
Next i
tgt(1, 1) = "EmpId": tgt(1, 2) = "Date": tgt(1, 3) = "Shift" ' get captions
' [4] write target array back wherever you want it to ' << redefine OFFSET
ws.Range("A1").Offset(0, c + 2).Resize(UBound(tgt, 1), UBound(tgt, 2)) = tgt
MsgBox "Time needed: " & Format(timer - t, "0.00") & " seconds."
End Sub
Note
You should format the target range with your preferred date formatting, e.g. "dd/mm/yyyy;#" .

Concatenate every other row in Excel

I have an Excel sheet that looks like this:
3 | latitude | 46.142737
3 | longitude| -57.608968
8 | latitude | 43.142737
8 | longitude| -52.608968
15 | latitude | 41.142737
15 | longitude| -59.608968
I need the end result to look like this:
3 | 46.142737, -57.608968
8 | 43.142737, -52.608968
15 | 41.142737, -59.608968
It can be concatenated based on every other row, or based on the integer value in the first column.
VBA suggestions? Thank you.
Edit: There is no actual "|" in my Excel sheet. The "|" is meant to be a visual cue representing a new column.
You could read the data into an array and then write that to a range
Original Data:
Result:
Code:
Sub Example()
Dim i As Long
Dim x As Long
Dim arry As Variant
ReDim arry(1 To 2, 1 To 1) As Variant
For i = 1 To ActiveSheet.UsedRange.Rows.Count
If Cells(i, 1).Row Mod 2 = 1 Then
x = x + 1
ReDim Preserve arry(1 To 2, 1 To x) As Variant
arry(1, x) = Cells(i, 1).Value
arry(2, x) = Cells(i, 3).Value & ", " & Cells(i + 1, 3).Value
End If
Next
arry = WorksheetFunction.Transpose(arry)
Sheets("Sheet2").Select
Range(Cells(1, 1), Cells(UBound(arry), UBound(arry, 2))).Value = arry
End Sub

Split 1 column content(comma separated) into multiple rows in EXCEL

I have some data like below,
UserID | UserName | skills |
1 | John | 1,2,3,4,5|
2 | Mary | 1,2,3|
Can anyone help me with a macro which can change the data structure into:
UserID | UserName | skills |
1 | John | 1 |
1 | John | 2 |
1 | John | 3 |
1 | John | 4 |
1 | John | 5 |
2 | Mary | 1 |
2 | Mary | 2 |
2 | Mary | 3 |
Thank you!
I've just had a minute to make this code for you. Some additional assumptions in comments below.
Sub qTest()
'assumptions:
'1. you need to select top left cell of your original data table, _
i.e. cell UserId
'2. table will be created to the right- there must be empty area
'select UserID cell
Dim i As Long
Dim tmpSkills As Variant
Dim tmpRow As Long
Dim iSkills As Long
Dim tmpArray As Variant
tmpArray = Selection.CurrentRegion
'copying
Selection.Resize(1, 3).Copy Selection.Offset(0, 4)
For i = 2 To UBound(tmpArray)
tmpSkills = Split(tmpArray(i, 3), ",")
iSkills = UBound(tmpSkills) +1
'skils
Selection.Offset(1 + tmpRow, 6).Resize(iSkills, 1) = Application.Transpose(tmpSkills)
'UserId
Selection.Offset(1 + tmpRow, 5).Resize(iSkills, 1) = tmpArray(i, 2)
'UserName
Selection.Offset(1 + tmpRow, 4).Resize(iSkills, 1) = tmpArray(i, 1)
tmpRow = tmpRow + iSkills
Next
End Sub
Picture presenting data before (on the left) and after (on the right). UserID cell should be selected before you run macro.
You can use the text to columns function in Excel.
Please refer to this link:
Microsoft Support
This method looks at each row and then inserts rows and spreads the information in place, overwriting. But I think I like KazJaw's better.
Sub Spread_Skills()
'Spread string of skills down spreadsheet for each UserID
'Application.ScreenUpdating = False 'Uncomment for large files
i = 2
Do While Not IsEmpty(Cells(i, 1)) 'as long as there is a userid do this
If Not InStr(Cells(i, 3), ",") = 0 Then 'if there is a comma, more than one skill, do this
UserId = Cells(i, 1) 'gather info
UserName = Cells(i, 2) 'gather info
adn = Len(Cells(i, 3)) - Len(Application.WorksheetFunction.Substitute(Cells(i, 3), ",", "")) 'count number of skills
Rows(i + 1 & ":" & i + adn).Select 'go to the next row
Selection.Insert Shift:=xlDown, CopyOrigin:=xlFormatFromLeftOrAbove 'Insert a row for each skill-1
temp = Mid(Cells(i, 3), Application.WorksheetFunction.Find(",", Cells(i, 3), 1) + 1, Len(Cells(i, 3))) 'asign string of skills
Cells(i, 3) = Left(Cells(i, 3), Application.WorksheetFunction.Find(",", Cells(i, 3), 1) - 1) 'make the first row the first skill
For o = i + 1 To i + adn 'for each additional skill do this
If Not InStr(temp, ",") = 0 Then 'if it isn't the last skill do this
ntemp = Left(temp, Application.WorksheetFunction.Find(",", temp, 1) - 1) 'slice
temp = Mid(temp, Application.WorksheetFunction.Find(",", temp, 1) + 1, Len(temp)) 'reasign remaining skills
Else: 'if it is the last skill do this
ntemp = temp
End If
Cells(o, 1) = UserId 'enter data
Cells(o, 2) = UserName 'enter data
Cells(o, 3) = ntemp 'enter data
Next o 'next row in skill range
End If
i = i + adn + 1 'go to the next userid
Loop
'Application.ScreenUpdating = true 'Uncomment for large files
End Sub

Resources