I'm relatively new to VBA and have some code I wrote that seems like it should be straightforward but is not behaving as expected. I am trying to separate my primary WorkSheet (GAWi) into three other worksheets (LWi, WMi, & OTi) based on the first letter in column H. Basically if the first letter is "L" I want that row to be copied and pasted onto sheet LWi and then deleted from the original sheet. Then if it is W it goes onto WMi and if it is A it goes onto OTi. It is functioning properly for the first two If statements (placing items that begin with L & W onto the correct sheets), but for the last one items that begin with P and 0 are also being placed onto sheet OTi. I'm at a complete loss, it seems pretty easy and I can't figure out where I went wrong. Any advice would be much appreciated, also I'm sure this code is pretty unelegant by most standards so any tips on how to shorten it would also be welcomed-I've just started getting into VBA in the last couple weeks. Thank so much!
Sheets("GAWi").Select
Columns("H:H").Select
Dim lwr As Range
Set lwr = ActiveSheet.UsedRange
For i = lwr.Cells.Count To 1 Step -1
If Left(lwr.Item(i).Value, 1) = "L" Then
lwr.Item(i).EntireRow.copy
Sheets("LWi").Select
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks:=False, Transpose:=False
ActiveCell.Offset(1, 0).Select
Sheets("GAWi").Select
lwr.Item(i).EntireRow.Delete
End If
If Left(lwr.Item(i).Value, 1) = "W" Then
lwr.Item(i).EntireRow.copy
Sheets("WMi").Select
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks:=False, Transpose:=False
ActiveCell.Offset(1, 0).Select
Sheets("GAWi").Select
lwr.Item(i).EntireRow.Delete
End If
If Left(lwr.Item(i).Value, 1) = "A" Then
lwr.Item(i).EntireRow.copy
Sheets("OTi").Select
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks:=False, Transpose:=False
ActiveCell.Offset(1, 0).Select
Sheets("GAWi").Select
lwr.Item(i).EntireRow.Delete
End If Next i
there's a main flaw in your logic: the use of UsedRange
despite being it a 2D range, its Item() property would act as if it were a 1D array with one row listed after another
so that were "A1:H10" (eight columns) the address of UsedRange, UsedRange.Item(1) would point to "A1", UsedRange.Item(8) would point to "H1" and UsedRange.Item(9) would point to "A2" …
so you have to loop through the cells of column H only
Then there's a coding flaw, which is the use of all those Select/Selection: get in the habit of always use explicit range reference qualified up to their parent worksheet and workbook
. This can be reached, for instance, with the use of With... End With construct
here's a possible code (explanations in comments):
Option Explicit
Sub TransferRows()
Dim i As Long
With Sheets("GAWi") ' reference "source" sheet
For i = .Cells(.Rows.Count, "H").End(xlUp).Row To 1 Step -1 ' loop backwards from referenced sheet column H last not empty cell row index to 1
Select Case UCase(.Cells(i, "H").Value) ' check for referenced sheet column H current row content
Case "L"
TransferRow Intersect(.UsedRange, .Rows(i)), Sheets("LWi") ' pass referenced sheet current row "used" range and "LWi" destination sheet to the helper sub
Case "W"
TransferRow Intersect(.UsedRange, .Rows(i)), Sheets("WMi") ' pass referenced sheet current row "used" range and "WMi" destination sheet to the helper sub
Case "A"
TransferRow Intersect(.UsedRange, .Rows(i)), Sheets("OTi") ' pass referenced sheet current row "used" range and "OTi" destination sheet to the helper sub
End Select
Next i
End With
End Sub
Sub TransferRow(sourceRng As Range, destSht As Worksheet)
With destSht
.Cells(.Rows.Count, 1).End(xlUp).Offset(1).Resize(, sourceRng.Columns.Count).Value = sourceRng.Value
End With
sourceRng.Delete xlUp
End Sub
As you see, other than the amendements due to the preface explanations I put in there:
the use of Select Case syntax instead of If Then End If
which I think is much clearer and would also correct a minor logic flaw of your orginal code: once a check is positive there's no need to run other ones (this you could have obtained by means of If - Then - ElseIf - Endif construct)
the use of a "helper" sub to demand the repetitive code to
which gives you much more control over your code and helps its maintenance
the use of Cells(Rows.Count, colIndex).End(xlUp) pattern
which is the most frequently used one to get the reference to the last not empty cell in some colIndex (be it a number or a letter) column
Thanks to HTH's great response I was able to clean up my code a bit and think I got it figured out. I opted to stick with the If Then Else If format since I am not too familiar with using Case yet. Here's the first section of it, I just repeated the copy, paste, delete row for each starting letter.
Set rng = Range("GAWi!H:H")
For k = rng.Cells.Count To 1 Step -1
If Left(rng.Item(k).Value, 1) = "W" Then
With rng.Item(k)
.EntireRow.copy
Sheets("WMi").Activate
ActiveCell.Offset(1, 0).PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks:=False, Transpose:=False
.EntireRow.Delete
End With
ElseIf Left(rng.Item(k).Value, 1) = "L" Then....
This is running well for my purposes but if anyone has more suggestions they are much appreciated.
Related
I just started working with large quantities of data from Azure and other software products that produce CSVs or excel files that produce information formatted as follows:
Is there an easy way to format it like:
So that it can be used effectively as a table?
Thank you!
Here's an algorithmic approach that you can implement in VBA
Get references to the Sheet and Range containig the data
Copy the Data to a Variant Array variable
Loop throught the Data Array rows
For each row that column A is Not Empty, capture the value
For each row where Column B is Empty, clear Column A
For each row where Column B is Not Empty, write the captured value to Column A
After the loop copy the Data Array back to the sheet
This works for me.
Sub TryMe()
' fill down from above
Range("A1:A20").Select ' change the range to suit your needs
Range("A20").Activate
Selection.SpecialCells(xlCellTypeBlanks).Select
Selection.FormulaR1C1 = "=R[-1]C"
' copy/paste values; no references to other cells
Cells.Select
Application.CutCopyMode = False
Selection.Copy
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks:=False, Transpose:=False
Range("A1").Select
' use come logic to do cleanup
Dim rng As Range, cell As Range
Set rng = Range("B1:B20") ' change the range to suit your needs
For Each cell In rng
If cell.Value = "" Then
cell.Offset(0, -1).Value = ""
End If
Next cell
End Sub
Before:
After:
Very new to VBA (and coding in general), trying to teach myself with the info that I can find online.
Writing a macro that will copy over employee performance data that is entered on the MAIN sheet, row by row, over to the LOG sheet of a workbook.
Up to that point, I've managed to make it work fine. But now I want to add a FIND function to determine whether a row of data already exists on the LOG sheet, so that I can then use an IF... THEN statement:
If data for a particular employee AND date does not exist yet, then that row is copied over to first empty row of the LOG sheet.
If it does exist already, the existing row of data on the LOG sheet will be overwritten.
This is what I've got:
Sub CopyToLog()
Dim RowCount As Integer
Sheets("MAIN").Select
For RowCount = 1 To Range("WeeklyData").Rows.Count
With Sheets("LOG").Range("A:B")
Set Dupe = .Find(Sheets("MAIN").Range("B5:C5").Offset(RowCount - 1, 0), LookIn:=xlValues)
Range("B5:F5").Offset(RowCount - 1, 0).Select
Selection.Copy
Sheets("LOG").Select
If Dupe Is Nothing Then
Range("A" & Rows.Count).End(xlUp).Offset(1).Select
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=False
Else
Dupe.Select
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=False
End If
End With
Sheets("MAIN").Select
Next RowCount
End Sub
This does not work however. It looks like the problem is in the Find function:
With Sheets("LOG").Range("A:B")
Set Dupe = .Find(Sheets("MAIN").Range("B5:C5").Offset(RowCount - 1, 0), LookIn:=xlValues)
I need to check for both Date and Employee (columns B & C on the MAIN sheet vs columns A & B on the LOG sheet), however it seems the formula as it is here is only comparing the first column (Date). As a result, data for one employee now gets overwritten by the next employee if it is for the same date.
Can you only use Find to find a single-cell value, not for a range of two neighbouring cells? If so, any tips on how to get around this?
It sounds like helper cells can accomplish what you're after. I'm assuming you have space in cell D5 next to Sheets("MAIN").Range("B5:C5"). Enter in the formula =B5&C5 into D5. Doing this will give you the combination of the two and allow you to achieve the lookup you want. You'll also need a helper column for your lookup range on your log sheet. If your information starts on row 10 in cell C10 enter =A10&B10, copy down as needed. With these in place you can now use them for your check. I altered your code to use the helper cells.
Sub CopyToLog()
Dim main As Worksheet
Set main = ThisWorkbook.Worksheet("MAIN")
Dim log As Worksheet
Set log = ThisWorkbook.Worksheets("LOG")
Dim searchRange As Range
Set searchRange = log.Range("C:C") 'Helper Column
Dim RowCount As Integer
For RowCount = 1 To main.Range("WeeklyData").Rows.Count
Dim lookFor As String
lookFor = main.Range("D5").Offset(RowCount - 1, 0).Value2 'Uses helper cells
Dim dupe As Range
Set dupe = searchRange.Find(lookFor, LookIn:=xlValues)
Dim copyInfo As Range
Set copyInfo = searchRange.Range("B5:F5").Offset(RowCount - 1, 0)
Dim destination As Range
If dupe Is Nothing Then
Set destination = log.Range("A" & Rows.Count).End(xlUp).Offset(1)
Else
Set destination = dupe
End If
destination.Resize(ColumnSize:=copyInfo.Columns.Count).Value2 = copyInfo.Value2
Next
main.Select
End Sub
Work with the objects themselves. I removed the .Select and Selection. pairs. Not using them will make your code more readable and faster. To get started the macro recorder will use them but be aware you can sandwich them out (IE Range("M10").Select followed by Selection.Copy is best written as Range("M10").Copy).
I replaced any unqualified Range variables with qualified ones. Range is implicity using the ActiveSheet, in essence it's ActiveSheet.Range. Qualifying it with a worksheet object like main.Range("A5") lets you know exactly which sheet it's coming from. This saves you from hair pulling moments later on.
destination.Resize(ColumnSize:=copyInfo.Columns.Count).Value2 = copyInfo.Value2 is doing the same as copying the values directly. Resize is used to make both ranges be the same size.
I have a question regarding excel vba and the use of the "IF" function with the "For" cycle. I will try to be as clear as possible.
here is the code:
sub cicleFor()
For i= 1 to 2
Calculate
Range("E3").Select
Selection.Copy Sheets("Sheet2").Select Range("A2").Select
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=False
Sheets("Sheet1").Select
Range("C14").Select
Next
It is pretty simple! I take E3, copy paste into another cell (A2) for two times.
I use calculate at the start because the number in E3 will change each time
What I would like to have is the following:
to use an "IF" function that, if A2 is full, goes to A3 and so on, for i = 1 to 100.
since I have used a for function, i want A2,A3,A4,...A100 to be filled with the result of E3 of the sheet1.
I am not an expert as you can see!
if you have any hint, I will be grateful!
Thank you!
EDIT to add the case of multiple cells
maybe you're after this
Sub cicleFor()
For i = 1 To 100
Calculate
Sheets("Sheet2").Cells(1 + i, 1).Value = Sheets("Sheet1").Range("E3").Value
Next
End Sub
if you need to copy/paste the content of more than one cell then you want to use Resize(nRows, nColumns) property of Range object to properly adjust the "target" range size to fit the "source" one
for instance to copy/paste the content of range "E3:H3" (i.e. four columns) you want to use:
Sub cicleFor()
For i = 1 To 100
Calculate
Sheets("Sheet2").Cells(1 + i, 1).Resize(,4).Value = Sheets("Sheet1").Range("E3:H3").Value
Next
End Sub
Your code could be much shorter - no need for select/copy/paste if you only want to transfer the values. Use End(xlUp) to locate the last used cell.
Sub cicleFor()
For i= 1 to 2
Calculate
Sheets("Sheet2").Cells(Rows.Count, 1).End(xlUp).Offset(1, 0).Value = _
Sheets("Sheet1").Range("E3").Value
Next
End Sub
Friends, I'm very poor in programming but maybe someone is willing to help.
My spreadsheet contains 18 ranges and 2 different headers in protected area. I need to copy and combine 1 header and 1 range to another, unprotected area. User should press a button and macro brings data to new position where it can be pasted.
For button operation I have an application.caller for hiding and showing rows. I think this is a good start. I also have a copy macro for 1 set of ranges. I'd like to combine these 2 in to a new macro.
Sub Macro_copy_RIVA1()
Range("RHEAD").Copy
Range("RIVA1").Copy
Application.Goto Reference:="R1120C2"
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks _
:=False, Transpose:=False
Application.CutCopyMode = False
Selection.Cut
End Sub
My application caller for hide/show rows is (thanks to Stackoverflow)
Sub ShowHideRows()
Dim arr
'split the calling button name into an array
' (array will be zero-based)
arr = Split(Application.Caller, "_")
'**EDIT** check array is expected size...
If UBound(arr) <> 3 Then Exit Sub
If IsNumeric(arr(1)) And IsNumeric(arr(2)) Then
With ActiveSheet ' "Me" if the code is in the sheet module, else "ActiveSheet"
.Unprotect Password:=""
'arr(1) determines start row
'arr(2) determines # of rows
'arr(3) determines if rows are hidden or not
.Cells(arr(1), 1).Resize(arr(2), 1).EntireRow.Hidden = (arr(3) = "H")
.Protect Password:=""
End With
End If
End Sub`
My ranges are called :
Header:
RHEAD1
RHEAD2
Ranges:
RIVA1
RIVA2
RIVA3
.....
RIVA6
RMVA1
RMVA2
.....
RMVA12
Proposed name of button : btn_RHEAD1_RIVA1 or btn_RHEAD2_RMVA12
How can I run a macro from an application caller that performs the copying task ?
Thanks
I currently have this code
Sheets("Pivot_Table_Non_Closed_Area").Range("E7:L7").Copy
'Pastes the data from the sheet above in the next avaliable row.
Sheets("Tracking_Table_Non_Closed_Area").Cells(Rows.Count, "C").End(xlUp).Offset(1). _
PasteSpecial Paste:=xlPasteValues, _
Operation:=xlNone, SkipBlanks:=False, Transpose:=False
Sheets("Tracking_Table_Non_Closed_Area").Select
n = Cells(Rows.Count, "C").End(xlUp).Row
Range("A" & n) = Date
Range("B" & n) = Time
This is how my current code presents it:
https://www.dropbox.com/s/p99kh0y3x2vsbo2/Currently_Presents.JPG?dl=0
but I can not seem to work out how to change it from copying rows of data and pasting rows into copying from columns of data and pasting into columns
This is how I want the new code to present the data:
https://www.dropbox.com/s/krkdjlculdqpckn/Wish_for_it_to_Be_Presented.JPG?dl=0
Hope this makes sense
Edit:
This is how my current code now looks after all the help, but stills struggling with the Date and time
Sheets("Pivot_Table_002").Range("B10:B19").Copy
Sheets("Sheet1").Cells(7, Columns.Count).End(xlToLeft).Offset(0, 1). _
PasteSpecial Paste:=xlPasteValues, _
Operation:=xlNone, SkipBlanks:=False, Transpose:=False
n = Cells(7, Columns.Count).End(xlToLeft).Column
Range("A" & n) = Date
Range("B" & n) = Time
Thanks
Edit - the Date and Time continued
You are setting the destination for Date and Time using a Range, but now that n represents the last-occupied column, you need to change that logic. Let's use the Cells construct, which I think reads better in this case:
Sheets("Tracking_Table_Non_Closed_Area").Cells(7, n) = Date
Sheets("Tracking_Table_Non_Closed_Area").Cells(8, n) = Time
Here's how .Cells is doing the work:
.Cells(row_identifier, column_identifier)
With that, you should be all set!
Edit - the Date and Time
Let's apply the same strategy to the Date and Time that we did to the column-ish data. The original design does the following:
n = Cells(Rows.Count, "C").End(xlUp).Row
What's actually happening there? n is a number. Specifically, n is the row number of the last-occupied cell in column "C". We're interested in getting the last-occupied column in a row instead -- let's say, to stick with the example below, we the last-occupied column in row 7:
n = Cells(7, Columns.Count).End(xlToLeft).Column
Boom! Now that n holds the last-occupied column number, you can apply the same strategy that you have in your last two lines to write in the Date and Time per the screenshots you provided.
Initial Answer:
I think a dissection of your already-existing code will help you along here, so let's get after it!
The copy/paste action is happening on these two lines:
'This line does the copying
Sheets("Pivot_Table_Non_Closed_Area").Range("E7:L7").Copy
'This line does the pasting
Sheets("Tracking_Table_Non_Closed_Area").Cells(Rows.Count, "C").End(xlUp).Offset(1). _
PasteSpecial Paste:=xlPasteValues, _
Operation:=xlNone, SkipBlanks:=False, Transpose:=False
(indentation added by me for clarity on the pasting line as _ is a multi-line indicator.)
Let's talk about the copy:
Sheets("Pivot_Table_Non_Closed_Area") '<~ this specifies the worksheet
Range("E7:L7") '<~ this specifies the range, which is a row-ish
' group of cells from E7 to L7
Copy '<~ this is the copy method
So, if you wanted to work with a column-ish group of cells instead, you'd adjust the Range. For the sake of an example, let's say you're interested in the column-ish group of 5 cells from E7 to E11. If you wanted to copy that group, you would write:
Sheets("Pivot_Table_Non_Closed_Area").Range("E7:E11").Copy
Nice! Now let's dive into the paste:
Sheets("Tracking_Table_Non_Closed_Area") '<~ this specifies the worksheet
Cells(Rows.Count, "C").End(xlUp).Offset(1) '<~ this starts in the last cell in
' column C (Rows.Count = the count
' of all the rows, i.e. 1 million-
' ish in Excel 2007+ or 56K-ish in
' Excel 2003). Then, .End(xlUp)
' simulates hitting Ctrl + Up on
' the keyboard, bringing you to the
' last occupied cell in column C.
' Finally, .Offset(1) increments
' that location by 1 row, bringing
' you to the cell immediately below
' the last occupied cell in
' column C.
PasteSpecial Paste:=xlPasteValues (then options) '<~ this does the pasting, with
' values-only (along with some
' other options, which aren't that
' important here.
Cool, right? Finding the last occupied row and writing information immediately below it is a cornerstone of VBA, so I would recommend reading this killer writeup on that subject. So what if you wanted to paste the column-ish area we copied above one column right of the last occupied column in row 7? We could write this:
Sheets("Tracking_Table_Non_Closed_Area").Cells(7, Columns.Count).End(xlToLeft).Offset(0, 1). _
PasteSpecial Paste:=xlPasteValues, _
Operation:=xlNone, SkipBlanks:=False, Transpose:=False
Hope that helps!