Using VBA If then statement to copy and paste data - vba

I am a brand new VBA user attempting to copy and paste data based on a range of dates. In column one I have dates and in column two I have the data I would like to copy and paste. CurYear refers to the end date in the range I am looking for and StatDate refers to the beginning date in the Range I am looking for. When I run this code it crashes Excel. Please help I am very lost
Worksheets("Weekly").Select
Dim nRows As Integer
Dim CurYear As Date
Dim StartDate As Date
nRows=Range("A1").CurrentRegions.Count.Rows
CurYear=Range("I265").Value
StartDate=Range("M5").Value
Do While Cells(nRows,1)<>""
if Cells(nRows,1).Value< CurYear & Cells(nRows,1)> StartDate Then
Cells(nRows,1).Offset(0,1).Copy
Worksheets("Weekly").Range("H41").Paste
Loop
End If

Put "option explicit" at the top of your code (before the sub) and it will tell you things to fix. Doing that will fix the part of your error where your end if was outside the loop instead of inside it but it won't catch that you weren't changing your loop counter. Try this code instead. It is actually pretty much the same as what you had with a couple minor changes.
Option Explicit
Sub test()
Dim sht As Worksheet, i As Long, l As Long, j
Dim nRows As Integer
Dim CurYear As Date
Dim StartDate As Date
Set sht = Worksheets("Test1") ' set the sheet as object isntead of selecting it for faster code and avoiding other issues
nRows = Cells(sht.Rows.Count, "B").End(xlUp).Row 'Last used row in column B - current region lastrow gets twitchy in some circumstances and should be avoided unless there is a reason to use it
l = 41
CurYear = range("I265").Value
StartDate = range("M5").Value
For i = 1 To nRows
If Cells(i, 1).Value < CurYear And Cells(i, 1).Value > StartDate Then 'for If statements you use "and" not "&"
Cells(l, 15) = Cells(i, 2) 'you will want something like this line and the next if you don't want to overwrite H41 if there is more than one match
l = l + 1
End If
Next i
End Sub
Also, to help with debugging, Open your locals window (View in the VBE). Step through your code with F8, watching your variables in the locals window to ensure that they are what you expect them to be at that step in your script.
If you do this with your code, you will see that you were missing a counter change with your variable for your loop. So it was looking for nRow to eventually be "" but it stays at whatever it was set to. Infinite loop. I changed it to a for next format but 6 of 1 and half dozen of another for your code.
Welcome to VBA. Don't poke yer eye out. :-)

Instead of using copy/ paste that uses a lot of memory and makes the program run slow, you maybe want to consider the following code that serves the same purpose as your code or Rodger's yet faster than using Select and copy/ paste syntax.
Sub Test()
Dim nRows As Long, LastRow As Long 'Declare as Long instead of Integer to avoid overflow
Dim CurYear As Date, StartDate As Date
LastRow = Cells(Rows.Count, 1).End(xlUp).Row 'Count the last used row in column 1 where you put the first data (dates)
nRows = 2 'Set the starting point of row where you put the first data (dates). In this example I use 2
CurYear = Range("I265").Value
StartDate = Range("M5").Value
Do
If Cells(nRows, 1).Value < CurYear And Cells(nRows, 1) > StartDate Then 'Use And not &
Cells(nRows, 5).Value = Cells(nRows, 2).Value 'This is essentially a "copy/ paste" syntax. Change the value (5) to the column you want to paste the value in column 2
End If
nRows = nRows + 1 'Set an increment value so each looping the nRows will increase by 1
Loop Until nRows = LastRow + 1 'Added by 1 so that the data in LastRow will keep being processed
End Sub

Related

Sum Values based on unique ID

Just started a new job. I'm automating a month-end report and I'm new at VBA. Been googling most of my issues with success, but I've finally run into a wall. In essence I'm downloading some data from SAP and from there I need to build a report.
My question is: How to do a sumif function using loops in VBA?
Data pull:
Sheet1 contains a product code and purchase amounts (columns A & B) respectively. One product code can have several purchases (several rows with the same product code).
Steps so far:
I arranged the data sheet1 to be in ascending order.
Copied unique values for the product codes onto another sheet (sheet2). So Sheet2 has a list of all the products (in ascending order).
I want to get the sum of all purchases in sheet2 column B (per product code). I know how to do this using formulas, but I need to automate this as much as possible. (+ I'm genuinely interested in figuring this out)
This is what I did in VBA so far:
Sub Macro_test()
Dim tb As Worksheet
Dim tb2 As Worksheet
Dim x As Integer
Dim y As Integer
Dim lrow As Long
Set tb = Sheets("sheet1")
Set tb2 = Sheets("sheet2")
lrow = tb.Cells(Rows.Count, "A").End(xlUp).Row
For x = 2 To lrow
For y = 2 To lrow
If tb2.Cells(x, 1).Value = tb.Cells(y, 1).Value Then
tb2.Cells(x, 2).Value = tb.Cells(y, 2).Value
End If
Next y
Next x
End Sub
If i'm not mistaken, for each product_code in sheet2 col A, I'm looping through all the product codes in sheet1 and getting back the LAST value it finds, instead of the sum of all values... I understand why it doesn't work, I just don't know how to fix it.
Any help would be much appreciated. Thanks!
This statement overwrites the value of tb2.Cells(x, 2).Value at each iteration:
tb2.Cells(x, 2).Value = tb.Cells(y, 2).Value
Instead, I think you need to keep adding to it:
tb2.Cells(x, 2).Value = tb2.Cells(x, 2).Value + tb.Cells(y, 2).Value
But I don't like the looks of your double-loop which uses only one lrow variable to represent the "last row" on the two different worksheets, that could be causing some issues.
Or, in your loop do something like this which I think will avoid the duplicate sum. Still, assumes the second worksheet doesn't initially have any value in
' Base our lRow on Sheet2, we don't care how many rows in Sheet1.
lrow = tb2.Cells(tb2.Rows.Count, 1).End(xlUp).Row
Dim cl as Range
Set cl = tb.Cells(2,1) 'Our initial cell value / ID
For x = 2 to lRow '## Look the rows on Sheet 2
'## Check if the cell on Sheet1 == cell on Sheet2
While cl.Value = tb2.Cells(x,1).Value
'## Add cl.Value t- the tb2 cell:
tb2.Cells(x, 2).Value = tb2.Cells(x, 2).Value + cl.Offset(0,1).Value
Set cl = cl.Offset(1) '## Reassign to the next Row
Wend
Next
But it would be better to omit the double-loop and simply use VBA to do 1 of the following:
1. Insert The Formula:
(See Scott Holtzman's answer).
This approach is better for lots of reasons, not the least of which is that the WorksheetFunction is optimized already, so it should arguably perform better though on a small dataset the difference in runtime will be negligible. The other reason is that it's stupid to reinvent the wheel unless you have a very good justification for doing so, so in this case, why write your own version of code that accomplishes what the built-in SumIf already does and is specifically designed to do?
This approach is also ideal if the reference data may change, as the cell formulas will automatically recalculate based on the data in Sheet1.
2. Evaluate the formula & replace with values only:
If you prefer not to retain the formula, then a simple Value assignment can remove the formula but retain the results:
With .Range(.Range("B2"), .Range("A2").End(xlDown).Offset(, 1))
.FormulaR1C1 = "=SUMIF(Sheet1!C[-1]:C[-1],RC[-1],Sheet1!C:C)"
.Value = .Value 'This line gets rid of the formula but retains the values
End With
Use this approach if you will be removing Sheet1, as removing the referents will break the formula on Sheet2, or if you otherwise want the Sheet2 to be a "snapshot" instead of a dynamic summation.
If you really need this automated, take advantage of VBA to place the formula for you. It's very quick and easy using R1C1 notation.
Complete code (tested):
Dim tb As Worksheet
Dim tb2 As Worksheet
Set tb = Sheets("sheet1")
Set tb2 = Sheets("sheet2")
Dim lrow As Long
lrow = tb.Cells(tb.Rows.Count, 1).End(xlUp).Row
tb.Range("A2:A" & lrow).Copy tb2.Range("A2")
With tb2
.Range("A2").CurrentRegion.RemoveDuplicates 1
With .Range(.Range("B2"), .Range("A2").End(xlDown).Offset(, 1))
.FormulaR1C1 = "=SUMIF(Sheet1!C[-1]:C[-1],RC[-1],Sheet1!C:C)"
End With
End With
Note that with R1C1 notation the C and R are not referring to column or row letters . Rather they are the column and row offsets from the place where the formula is stored on the specific worksheet. In this case Sheet!C[-1] refers to the entire A column of sheet one, since the formula is entered into column B of sheet 2.
I wrote a neat little algorithm (if you can call it that) that does what you want them spits out grouped by totals into another sheet. Basically it loops through the first section to get unique names/labels and stores them into an array. Then it iterates through that array and adds up values if the current iteration matches what the current iteration of the nested loop position.
Private Sub that()
Dim this As Variant
Dim that(9, 1) As String
Dim rowC As Long
Dim colC As Long
this = ThisWorkbook.Sheets("Sheet4").UsedRange
rowC = ThisWorkbook.Sheets("Sheet4").UsedRange.Rows.Count
colC = ThisWorkbook.Sheets("Sheet4").UsedRange.Columns.Count
Dim thisname As String
Dim i As Long
Dim y As Long
Dim x As Long
For i = LBound(this, 1) To UBound(this, 1)
thisname = this(i, 1)
For x = LBound(that, 1) To UBound(that, 1)
If thisname = that(x, 0) Then
Exit For
ElseIf thisname <> that(x, 0) And that(x, 0) = vbNullString Then
that(x, 0) = thisname
Exit For
End If
Next x
Next i
For i = LBound(that, 1) To UBound(that, 1)
thisname = that(i, 0)
For j = LBound(this, 1) To UBound(this, 1)
If this(j, 1) = thisname Then
thisvalue = thisvalue + this(j, 2)
End If
Next j
that(i, 1) = thisvalue
thisvalue = 0
Next i
ThisWorkbook.Sheets("sheet5").Range(ThisWorkbook.Sheets("Sheet5").Cells(1, 1), ThisWorkbook.Sheets("Sheet5").Cells(rowC, colC)).Value2 = that
End Sub
Yay arrays

Applying VBA RIGHT to an entire column - Infinite Loop Issue

I have data that I am working to Parse Out that I have imported from approval emails sent in Outlook. At this point I am just importing the CreationTime and the SubjectLine.
For the subject line I am able to use the Split function to separate out most of the data. I then am left with Job Codes in Column B and Position numbers in Column C which includes the text: "Job Codes: XXXX" and the four digit job code number and "PN XXXX" and either a four digit or 6 digit position number. I am trying to use the Right functionality to loop through the entire column and reformat the column just to show only the four digit job code number for Column B and either just the 4 digit or 6 digit position number (the actual numbers) for Column C
For Job Code Column B:
Currently my code works for Shortening the Job Codes but it involves adding a column, putting the RIGHT formula in that column for the shortened Job Code, then copying and pasting the formula as values back into the column and then deleting the original column.
The problem- Works but perhaps not the most efficient with a larger data set (currently 200 rows but will have 2000 or more)
Code:
Sub ShortenJobCodes()
Application.ScreenUpdating = False
Const R4Col = "=RIGHT(RC3,4)"
Dim oRng As Range
Dim LastRow As Long
Range("B1").EntireColumn.Insert
LastRow = Cells(Rows.Count, "A").End(xlUp).Row
Set oRng = Range("B:B")
Range(oRng, Cells(LastRow, "B")).FormulaR1C1 = R4Col
Set oRng = Nothing
Columns("B").Select
Selection.Copy
Selection.PasteSpecial Paste:=xlPasteValues
Range("C1").EntireColumn.Delete
Application.ScreenUpdating = True
End Sub
For Position Numbers Column C:
Currently I have mirrored the above code but added in an if statement using LEN to count if the characters are less than 8, if so then insert one RIGHT function if not insert the other RIGHT function. This also involves adding an additional column putting the RIGHT formula in that column for the shortened Position Number(Eliminating all but just the number), then copying and pasting the formula as values back into the column and then deleting the original column.
Problem - This works but seems to take forever to process and in fact looks like it is in an infinite loop. When I Esc out of it, it does add the column and then input the proper RIGHT formula (leaving just the numeric values) but the sub never seems to end, nor does it copy and paste the formulas as values or delete the original column. As noted above I realize this is likely a more efficient way to do this but I have tried a bunch of options without any luck.
I am realizing part of the loop might be due to the range itself being an entire column but I cannot find a way to stop that with the last row (even though I have a count in there).
Code:
Sub ShortenPositionNumbers()
Application.ScreenUpdating = False
Const R4Col = "=RIGHT(RC4,4)"
Const R6Col = "=RIGHT(RC4,6)"
Dim oRng As Range
Dim rVal As String
Dim y As Integer
Dim selCol As Range
Dim LastRow As Long
Range("C1").EntireColumn.Insert
LastRow = Cells(Rows.Count, "A").End(xlUp).Row
Set selCol = Range("D:D")
For Each oRng In selCol
oRng.Select
rVal = oRng.Value
If Len(oRng.Value) > 8 Then
oRng.Offset(0, -1).FormulaR1C1 = R6Col
Else
oRng.Offset(0, -1).FormulaR1C1 = R4Col
End If
Next
Set oRng = Nothing
Columns("C").Select
Selection.Copy
Selection.PasteSpecial Paste:=xlPasteValues
Range("D1").EntireColumn.Delete
Application.ScreenUpdating = True
End Sub
Major Question: Is there a way to use RIGHT/TRIM/LEN/LEFT functions to do this within a cell without having to add columns/delete columns and insert functions?
There are a few things you can do here to speed up your code. I'm only going to reference the second code block as you can apply similar logic to the first.
The first issue is that you create a LastRow variable but never reference it again. It looks like you meant to use this in the selCol range. You should change that line to Set selCol = Range("C1:C" & lastRow). This way, when you loop through the rows you only loop through the used rows.
Next, in the For-Each loop you Select every cell you loop through. There really isn't any reason to do this and takes substantially longer. You then create the variable rVal but never use it again. A better way to set up the loop is as follows.
For Each oRng in selCol
rVal = oRng.Value
If Len(rVal) > 8 Then
oRng.Value = Right(rVal, 6)
Else
oRng.Value = Right(rVal, 4)
End If
Next
This is much cleaner and no longer requires creating columns or copying and pasting.
Try this, it uses Evaluate and no loops or added columns.
Sub ShortenPositionNumbers()
Application.ScreenUpdating = False
Dim selCol As Range
Dim LastRow As Long
With ActiveSheet
LastRow = .Cells(Rows.Count, "A").End(xlUp).Row
Set selCol = .Range(.Cells(1, 3), .Cells(LastRow, 3))
selCol.Value = .Evaluate("INDEX(IF(LEN(" & selCol.Address(0, 0) & ")>8,RIGHT(" & selCol.Address(0, 0) & ",6),RIGHT(" & selCol.Address(0, 0) & ",4)),)")
End With
Application.ScreenUpdating = True
End Sub
Or work with arrays
Sub ShortenPositionNumbers()
Dim data As Variant
Dim i As Long
With Range("C3:C" & Cells(Rows.Count, "A").End(xlUp).Row)
data = Application.Transpose(.Value)
For i = LBound(data) to UBound(data)
If Len(data(i)) > 8 Then
data(i) = RIGHT(data(i),6)
Else
data(i) = RIGHT(data(i),4)
End If
Next
.Value = Application.Transpose(data)
End With
End Sub

Copy and Paste row by index number in Excel Macro

I'm trying to copy an entire row by index number and paste it to another row with a different index number when a certain condition is met (I know the issue is not with the conditional logic). I'm thinking of something like this:
Sub Makro1()
Dim i As Integer
With ActiveSheet
'for looping
totalRows = .Cells(.Rows.Count, "A").End(xlUp).Row
'index of last row even after rows have been added
lastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
'data starts at row #3
For i = 3 To totalRows
If .Cells(i, 19).Value > 0 Then
Number = .Cells(i, 19).Value
Do While Number > 0
lastRow = lasRow + 1
'Next line doesnt do anything
.Rows(lastRow) = .Rows(i).Value
Number = Number - 1
Loop
End If
Next i
End With
End Sub
The logic works like its supposed to but no lines are pasted. I've gone step by step and am certain the problem is not with the logic.
I assume that you want to copy Rows(i) and paste it as value in Rows(lastRow). So, you need to replace this line
.Rows(lastRow) = .Rows(i).Value
with these two lines:
.Rows(i).Copy
.Rows(lastRow).PasteSpecial xlPasteValues
Or
.Rows(lastRow).Copy
.Rows(i).PasteSpecial xlPasteValues
if you want to copy Rows(lastRow) and paste it as value in Rows(i).
Edit:
To paste everything (formulas + values + formats), use paste type as xlPasteAll.
Reference: msdn
Range Copy and Paste
Syntax
Range().Copy [Destination]
The square brackets indicate that Destination is an optional parameter. If you don't designate a Destination range it copies the selection to the clipboard. Otherwise it copies the first range directly to the new location.
Change this line:
.Rows(lastRow) = .Rows(i).Value
To:
.Rows(lastRow).copy .Rows(i)
It's worth noting that
.Rows(lastRow).copy .Cells(i, 1)
Will also work. Excel will resize the Destination range to fit the new data.
your code works for me
so just add a breakpoint at .Rows(lastRow) = .Rows(i).Value statement and then query all relevant variables value in the Immediate Window, like:
?lastRow
?.Rows(lastRow).Address
?i
?.Rows(i).Address
in the meanwhile you could
add Option Explicit statement at the very top of your code module
this will force you to declare all variables and thus lead to some extra work, but you'll get repaid with much more control over your variables usage and misspelling, thus saving debugging time
dim variables to hold rows index as of Long type, to handle rows index higher then 32767
avoid inner loop using the Resize() method of range object
much like follows:
Option Explicit
Sub Makro1()
Dim i As Long, totalRows As Long, lastRow As Long, Number As Long
With ActiveSheet
'for looping
totalRows = .Cells(.Rows.Count, "A").End(xlUp).Row
'index of row to add from
lastRow = totalRows + 1 '<--| start pasting values one row below the last non empty one in column "A"
'data starts at row #3
For i = 3 To totalRows
If .Cells(i, 19).Value > 0 Then
Number = .Cells(i, 19).Value
.Rows(lastRow).Resize(Number).Value = .Rows(i).Value
lastRow = lastRow + Number
End If
Next i
End With
End Sub

VBA Replace is Ignoring Column/Sheet Restrictions

I'm trying to use VBA for a find/replace. The goal is to iterate through a "Data_Pairs" sheet which contains all the pairs to find/replace, and to find/replace those pairs only in Column A and only in a specified range of sheets in the workbook (which does not include "Data_Pairs").
For some reason, every matching value is replaced, regardless of which column it's in. Values are also replaced in sheets whose index falls outside the defined range.
Any help would be greatly appreciated.
I'm using the following code:
Sub Replace_Names()
Dim row As Integer
Dim row2 As Integer
Dim sheet As Integer
Dim findThisValue As String
Dim replaceWithThisValue As String
For row = 1 To 10
Worksheets("Data_Pairs").Activate
findThisValue = Cells(row, "A").Value
replaceWithThisValue = Cells(row, "B").Value
For sheet = 2 To 10
Worksheets(sheet).Columns("A").Replace What:= findThisValue, Replacement:=replaceWithThisValue
Next sheet
Next row
End Sub
To give a concrete example of the issue: if Data_Pairs A1 = A and Data_Pairs B1 = 1, every single value of 1 in the entire workbook is replaced with A.
I observe this works as-expected in Excel 2010, echoing Greg and chancea's comments above.
HOWEVER, I also observe that if you have previously opened the FIND dialog (for example you were doing some manual find/replace operations) and changed scope to WORKBOOK, then the observed discrepancies will occur, as discussed here:
http://www.ozgrid.com/forum/showthread.php?t=118754
This may be an oversight, because it does not appear to have ever been addressed. While the Replace dialog allows you to specify Workbook versus Worksheet, there is no corresponding argument you can pass to the Replace method (documentation).
Implement the hack from the Ozgrid thread -- for some reason, executing the .Find method seems to reset that. This appears to work:
Sub Replace_Names()
Dim row As Integer
Dim row2 As Integer
Dim sheet As Integer
Dim findThisValue As String
Dim replaceWithThisValue As String
Dim rng As Range
For row = 1 To 10
Worksheets("Data_Pairs").Activate
findThisValue = Cells(row, "A").Value
replaceWithThisValue = Cells(row, "B").Value
For sheet = 2 To 3
Set rng = Worksheets(sheet).Range("A:A")
rng.Find ("*") '### HACK
rng.Replace What:=findThisValue, Replacement:=replaceWithThisValue
Next sheet
Next row
End Sub
You have a Worksheets("Data_Pairs").Activate inside your For ... Next loop. That would seem to indicate that the command is called 9× more that it has to be. Better not to reply on .Activate to provide the default parent of Cells.
Sub Replace_Names()
Dim rw As long, ws As long
Dim findThis As String, replaceWith As String
with Worksheets(1)
For rw = 1 To 10
findThis = .Cells(rw , "A").Value
replaceWith = .Cells(rw , "B").Value
For ws = 2 To 10 ' or sheets.count ?
with Worksheets(ws)
.Columns("A").Replace What:= findThis, Replacement:=replaceWith
end with
Next ws
Next rw
end with
End Sub
See How to avoid using Select in Excel VBA macros for more on getting away from Select and Acticate.

Extremely slow VBA code when formatting cells

When attempting to format 2 columns in a workbook, the execution of the macro is extremely slow. To format approximately 4000 rows, it takes over 10 minutes.
The dates are populated from an external source that stores them as strings.
When commenting the code, it loads under 60 seconds.
The code
'Discover last row of data
RowsToProcess = Range("A" & Rows.Count).End(xlUp).Row
For i = 6 To RowsToProcess
Worksheets("Data").Range("B" & i).Select
Selection.NumberFormat = "dd/mm/yy;;"
Selection.Value = CDate(Selection.Value)
Worksheets("Data").Range("C" & i).Select
Selection.NumberFormat = "dd/mm/yy;;"
Selection.Value = CDate(Selection.Value)
Next i
The code below does not format cells in the required format either.
Worksheets("Data).Columns("C").NumberFormat = dd/mm/yy;;"
The post #aelgoa linked to is spot on. When the standard Application.ScreenUpdating options for speeding up your code aren't enough, I turn to Variant arrays.
(If you wanted to see how I use Application.ScreenUpdating etc., wrapped in a GoFast function, check out my answer here: VBA code optimization)
The script below works like this:
Load the Range defined in columns B and C into a Variant array
Apply CDate logic there (rather than accessing the Sheet every time)
Write the CDate-modified array out to Sheet
One caveat though -- my question in the comment above about differentiating between mm/dd and dd/mm (say May 6th, 2014 vs June 5th, 2014) still stands. I'll modify the code below based on your thoughts there. Thanks!
Option Explicit
Sub ProcessDates()
Dim AryColBandC As Variant
Dim DateFormatB As Date, DateFormatC As Date
Dim RngColBandC As Range
Dim LastRow As Long, Counter As Long
Dim MySheet As Worksheet
'set references up-front
Set MySheet = ThisWorkbook.Worksheets("Sheet1")
With MySheet
LastRow = .Range("A" & .Rows.Count).End(xlUp).Row
Set RngColBandC = .Range(.Cells(6, 2), .Cells(LastRow, 3))
End With
'load the B-C column range into a variant array
AryColBandC = RngColBandC
'loop through the variant array, applying the date
'conversion to each entry in the array and writing back
For Counter = LBound(AryColBandC) To UBound(AryColBandC)
DateFormatB = CDate(AryColBandC(Counter, 1)) '<~ temporarily store
DateFormatC = CDate(AryColBandC(Counter, 2)) '<~ dates here
AryColBandC(Counter, 1) = DateFormatB
AryColBandC(Counter, 2) = DateFormatC
Next Counter
'write the results out to the sheet
For Counter = LBound(AryColBandC) To UBound(AryColBandC)
MySheet.Cells(5 + Counter, 2) = AryColBandC(Counter, 1)
MySheet.Cells(5 + Counter, 3) = AryColBandC(Counter, 2)
Next Counter
End Sub