How to copy every row except every nth - vba

In excel I would like to copy the date from one sheet to another one using macro in a way that it will copy everything until row 9, then it will skip row 10 and copy row 11 and 12, and then skip one again.
So it should not copy row 10,13,16,19, etc..
I have the following code
Dim i As Integer
i = 9
J = 1
K = 9
Do While i < 5000
If J = 3 Then
J = 0
Sheets("sheet1").Select
Rows(i).Select
Selection.Copy
Sheets("sheet2").Select
Cells(K, 1).Select
ActiveSheet.Paste
K = K + 1
End If
J = J + 1
i = i + 1
Loop
This code is copying everything till the 8th row and then every 3rd, can somebody help me how to modify that code?

Fastest way will be to Copy >> Paste the entire rows once, according to your criteria.
You can achieve it by merging all rows that needs to be copies to a Range object, in my code it's CopyRng, and you do that by using Application.Union.
Code
Option Explicit
Sub CopyCertailRows()
Dim i As Long
Dim CopyRng As Range
Application.ScreenUpdating = False
With Sheets("sheet1")
' first add the first 8 rows to the copied range
Set CopyRng = .Rows("1:8")
For i = 9 To 5000
If (i / 3) - Int(i / 3) <> 0 Then ' don't add to copied range the rows that divide by 3 without a remainder
Set CopyRng = Application.Union(CopyRng, .Rows(i))
End If
Next i
End With
' copy >> paste in 1- line
CopyRng.Copy Destination:=Sheets("sheet2").Range("A9")
Application.ScreenUpdating = True
End Sub

You could simplify this massively by using If i < 10 Or (i - 1) Mod 3 <> 0 Then... which will select the rows you're interested in. Like so:
Dim i As Integer, j As Integer
j = 0
Dim sourceSht As Worksheet
Dim destSht As Worksheet
Set sourceSht = Sheets("Sheet1")
Set destSht = Sheets("Sheet2")
For i = 1 To 5000
If i < 10 Or (i - 1) Mod 3 <> 0 Then
j = j + 1
sourceSht.Rows(i).Copy destSht.Rows(j)
End If
Next
Personally, I'd turn screen updating and calculations off before running this and enable them again after to reduce the time needed to perform the loop.
Also, as MichaƂ suggests, unless your dataset happens to be exactly 5,000 rows, you might want to 'find' the last row of data before starting to further reduce the time needed.

All necessary comments in code:
'declare all variables, be consistent with lower/uppercases, use Long instead of Integeer (its stored as long anyway)
'use meaningful variable names
Dim i As Long, copyUntil As Long, currentRow As Long
copyUntil = 9
currentRow = 1
'copy all rows until we reach 9th row
For i = 1 To copyUntil
Sheets("sheet1").Rows(i).Copy
Sheets("sheet2").Rows(currentRow).Paste
currentRow = currentRow + 1
Next
'now we will takes steps by 3, on every loop we will copy i-th row and next one, third will be omitted
'we also use currentRow variable to avoid empty rows in sheet2
'also, 5000 seems wrong, I'd recommend to determine last row, until which we will loop
'last row is often determined like Cells(Rows.Count, 1).End(xlUp).Row
For i = copyUntil + 2 To 5000 Step 3
Sheets("sheet1").Rows(i).Copy
Sheets("sheet2").Rows(currentRow).Paste
currentRow = currentRow + 1
Sheets("sheet1").Rows(i + 1).Copy
Sheets("sheet2").Rows(currentRow).Paste
currentRow = currentRow + 1
Next

This code will only paste values. Let me know if any questions or if you really, really need the formatting I can tweak it.
Sub DoCopy()
'This code is pretty much specifit to your request/question, it will copy 1-9, skip 10, 13, 16....
'i for the loop, x for the row that will not be added, y to paste on the second sheet
Dim i, x, y As Long, divn As Integer
For i = 1 To 5000
If i < 10 Then
y = y + 1
Sheets("Sheet1").Rows(i).Copy
Sheets("Sheet2").Range("A" & y).PasteSpecial ''Paste values only
ElseIf i >= 10 Then
x = i - 10
If x Mod 3 <> 0 Then
y = y + 1
Sheets("Sheet1").Rows(i).Copy
Sheets("Sheet2").Range("A" & y).PasteSpecial ''Paste values only
Else
'Do nothing
End If
End If
Next i
End Sub

Related

Auto scheduling

I am trying to make an auto scheduling program with an excel.
For example, each number is certain job assigned to the person given day.
1/2 1/3 1/4 1/5
Tom 1 2 2 ?
Justin 2 3 1 ?
Mary 3 3 ?
Sam 1 ?
Check O O X ? ## check is like =if(b2=c2,"O","X")
The things I want to make sure is every person is given a different job from yesterday.
My idea
while
randomly distribute jobs for 1/5
wend CheckCell = "O"
But I found that checking cell in the vba script doesn't work - the cell is not updated in each while loop.
Could you give me a little pointer for these kinds of program? Because I am new to vbaScript, any kinds of help would be appreciated.
Using VBA, I'm sure there are better ways to do this, but this will check the values from the penultimate column against values from last column and if they match it will write "O" to under the last column, else it will write "X":
Sub foo()
Dim ws As Worksheet: Set ws = Sheets("Sheet1")
'declare and set your worksheet, amend as required
LastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
'get the last row with data on Column A
LastCol = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
counter = 0 'set counter
For i = 2 To LastRow 'loop through penultimate column and add values to array
If ws.Cells(i, LastCol - 1).Value <> "" Then
Values = Values & ws.Cells(i, LastCol - 1) & ","
End If
Next i
Values = Left(Values, Len(Values) - 1)
Values = Split(Values, ",") 'split values into array
For i = 2 To LastRow 'loop through last column and add values to array
If ws.Cells(i, LastCol).Value <> "" Then
ValuesCheck = ValuesCheck & ws.Cells(i, LastCol) & ","
End If
Next i
ValuesCheck = Left(ValuesCheck, Len(ValuesCheck) - 1)
ValuesCheck = Split(ValuesCheck, ",")
For y = LBound(Values) To UBound(Values) 'loop through both arrays to find all values match
For x = LBound(ValuesCheck) To UBound(ValuesCheck)
If Values(y) = ValuesCheck(x) Then counter = counter + 1
Next x
Next y
If counter = UBound(Values) + 1 Then 'if values match
ws.Cells(LastRow + 1, LastCol).Value = "O"
Else 'else write X
ws.Cells(LastRow + 1, LastCol).Value = "X"
End If
End Sub
just to clarify are you looking to implement the random number in the vba or the check.
To do the check the best way would be to set the area as a range and then check each using the cells(r,c) code, like below
Sub checker()
Dim rng As Range
Dim r As Integer, c As Integer
Set rng = Selection
For r = 1 To rng.Rows.Count
For c = 1 To rng.Columns.Count
If rng.Cells(r, c) = rng.Cells(r, c + 1) Then
rng.Cells(r, c).Interior.Color = RGB(255, 0, 0)
End If
Next c
Next r
End Sub
this macro with check the text you have selected for the issue and change the cell red if it matches the value to the right.
To make it work for you change set rng = selection to your range and change the rng.Cells(r, c).Interior.Color = RGB(255, 0, 0) to the action you want
A sligthly different approach than the other answers.
Add this function:
Function PickJob(AvailableJobs As String, AvoidJob As String)
Dim MaxTries As Integer
Dim RandomJob As String
Dim Jobs() As String
Jobs = Split(AvailableJobs, ",")
MaxTries = 100
Do
MaxTries = MaxTries - 1
If MaxTries = 0 Then
MsgBox "Could find fitting job"
End
End If
RandomJob = Jobs(Int((1 + UBound(Jobs)) * Rnd()))
Loop Until RandomJob <> AvoidJob
PickJob = RandomJob
End Function
And put this formula in your sheet
=PickJob("1,2,3",D2)
where D2 points to is the previous job

Go through Cells and Round to Closest 5 VBA

I have a spreadsheet with 50K values on it.
I want it a code to go through every value and check to see if it ends in a 5 or 0 and if it doesn't not to round to the nearest of the two.
I tried this as my code
Sub Round_flow()
Dim nxtRow As Long, found As Boolean, i As Long, minus As Long, plus As Long, equal As Long, cell As Boolean, f As Integer
nxtRow = 2
found = False
i = Sheet1.Cells(nxtRow, 2)
minus = -2
equal = 0
While Not found 'finds last used row
If (Cells(nxtRow, 2) = "") Then
found = True
Else
nxtRow = nxtRow + 1
End If
Wend
For f = 2 To i
For minus = -2 To 168 Step 5
If ActiveCell.Value <> equal Then
While Not cell
plus = minus + 4
equal = minus + 2
If minus <= ActiveCell.Value <= plus Then
Sheet1.Cells(i, 2).Value = equal
cell = True
End If
Wend
End If
Next minus
Next f
Essentially what I was trying to do is say here is the last row, i want to check every value from i to last filled row to see if it falls between any plus and minus value (+-2 of the nearest 5 or 0) then have whatever activecell.value be replaced by the 0 or 5 ending digit 'equal' which changes with each iteration.
Ok, that seems way too complicated. To round to 5, you just multiply by 2, round, then divide by 2. Something like this will do the trick:
Dim NumberToBeRounded as Integer
Round(NumberToBeRounded *2/10,0)/2*10
*2 and /2 to get it to be rounded to 5, and /10 *10 to make the round function round for less than 0 decimals.
(I have to admit, I don't really understand what your code is trying to do, I hope I didn't completely misunderstand your needs.)
This should do the trick:
Sub Round_flow()
For f = 2 To Cells(1, 2).End(xlDown).Row
Cells(f, 2).Value = Round(Cells(f, 2).Value * 2 / 10) / 2 * 10
Next
End Sub
Cells(1, 2).End(xlDown).Row finds the last used cell, unless you have no data; if that can happen, add some code to check if you have at least 2 rows. Or you can use the Usedrange and SpecialCells(xlLastCell) combo to find the last used row of your table...
Another way:
Sub RoundEm()
Dim wks As Worksheet
Dim r As Range
Dim cell As Range
Set wks = ActiveSheet ' or any other sheet
On Error Resume Next
Set r = wks.Cells.SpecialCells(xlCellTypeConstants, xlNumbers)
On Error GoTo 0
If Not r Is Nothing Then
For Each cell In r
cell.Value2 = Round(cell.Value2 / 5, 0) * 5
Next cell
End If
End Sub

VBA to CountBlank the top 10 rows of every column (1 to 50)

I am trying to get the below code to work. I would like to count blank cells for columns 1 to 50 in each worksheet. The below works, but it counts for the entire column. How can I change it to only count the first 10 rows in each column and if they are all blank, then change the column width to 1?
Many thanks
For j = 1 To 50
Blanks = WorksheetFunction.CountBlank(Worksheet.Columns(j))
If Blanks > 10 Then
ws.Columns(j).ColumnWidth = 1
End If
Next j
If you only want to check the first 10 rows you need to specify this in your CountBlank function. Your CountBlank(Worksheet.Columns(j)) is counting the entire column.
Also, your If Blanks > 10 Then will never evaluate to True as you only want to count 10 rows. I've changed that expression to If Blanks = 10.
Sub countTest()
Dim wks As Worksheet
Set wks = Worksheets("Sheet1")
For j = 1 To 50
Blanks = WorksheetFunction.CountBlank(wks.Range(Cells(1, j), Cells(10, j)))
If Blanks = 10 Then
wks.Columns(j).ColumnWidth = 1
End If
Next j
Set wks = Nothing
End Sub
Try something like this:
Dim i As Integer
Dim j As Integer
Dim k As Integer
For i = 1 To 50
k=0
For j = 1 to 10
If Activesheet.Cells(j,i).Value = "" Then
k=k+1
End If
Next j
If k = 10 Then
Activesheet.Columns(i).ColumnWidth = 1
End If
Next i
Let me know if there are any issues with it.

VBA and Excel optimization of script, dealing with 700,000 rows

Hello StackOverflowers,
I am currently working on a script that has one nested IF statement in it. When run it could potentially compute around 1.4m IF's.
I have run a test with a timer (not too sure on the accuracy of the timer in VBA) and crunching 1000 rows gives me a time of 10 seconds. 10 * 700 = 7000 seconds, which = 1.94 hours.
Can anyone give me any tips for optimisation when dealing with such large data sets?
My code is as follows
Sub itS1Capped()
Dim Start, Finish, TotalTime
Start = Timer
Dim c, d, j, lastRow
c = 1
'find how many rows
With Worksheets("Data")
lastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
End With
'loop through all rows
For Each d In Worksheets("Data").Range("D2:D" & lastRow).Cells 'd = IT S0 Uncapped
j = Worksheets("Data").Range("J" & c + 1).Value 'IT Cap
If j <> 0 Then
If d > j Then
Worksheets("Data").Range("K" & c + 1).Value = j 'IT S1 Capped = j
Else
Worksheets("Data").Range("K" & c + 1).Value = d 'IT S1 Capped = d
End If
Else
Worksheets("Data").Range("K" & c + 1).Value = d 'IT S1 Capped = d
End If
c = c + 1
Next
Finish = Timer
TotalTime = Finish - Start
MsgBox TotalTime
End Sub
So I took inspiration from Mark Moore's use of arrays and found that using an array function rather than copying and pasting a plain function across a range is much faster. On my machine, Mark's procedure runs in 2.2 seconds, and the one below in 1.4 seconds.
Sub FormulaArray()
Dim iUsedRows As Long, rCell As Range, StartTimer As Double, Duration As Double
StartTimer = Timer
iUsedRows = ActiveSheet.UsedRange.Cells(ActiveSheet.UsedRange.Rows.Count, 1).Row
With Range(Cells(1, 11), Cells(iUsedRows, 11))
.FormulaArray = "=IF(J:J<>0,IF(D:D>J:J,J:J,D:D),D:D)"
.Copy
.PasteSpecial xlPasteValues
End With
Duration = StartTimer - Timer
MsgBox Format(Duration, "#0.0000") & " seconds to run"
End Sub
I am a bit old school and so "arrays" are your friend :-) Have had similar problems when I first took over looking after some pretty complex spreadsheets at work that did large numbers of validations. When working with large volumes of data, moving between the workbook and the data on the worksheet is not recommended, because each action is effectively an I/O (Input/ output) operation and these are very time consuming. It is massively more efficient to read all your data into an array, work with the array data and then write it back to the sheet at the end, this is effectively 2 I/O's instead of the 700,000 if you read the sheet data each time. As a rough example, I reduced our previous validation time down from 25 minutes to 4 seconds using this approach.
Sub ValidateSheet()
Dim DataRange As String
Dim SheetArray As Variant
Dim StartCol As String
Dim EndCol As String
Dim StartRow As Long ' long to cope with over 32k records
Dim lastrow As Long
Dim WorksheetToRead As String
Dim ArrayLoopCounter As Long
Dim Start, Finish, TotalTime
Start = Timer
'I use variables for the data range simply to allow it to be changed easily. My real code is actually paramatised so a single reusable procedure
'is used to populate all my arrays
'find how many rows
WorksheetToRead = "Data"
StartCol = "A"
EndCol = "Z"
StartRow = 1
lastrow = Sheets(WorksheetToRead).Cells(Rows.Count, "A").End(xlUp).Row
'set the range to be read into the array
DataRange = StartCol & Trim(Str(StartRow)) & ":" & EndCol & Trim(Str(StartRow - 1 + lastrow))
SheetArray = Worksheets(WorksheetToRead).Range(DataRange).Value ' read all the values at once from the Excel grid, put into an array
'Loop around the data
For ArrayLoopCounter = LBound(SheetArray, 1) To UBound(SheetArray, 1)
If SheetArray(ArrayLoopCounter, 10) <> 0 Then '10 is column J
'Compare D with J
If SheetArray(ArrayLoopCounter, 4) > SheetArray(ArrayLoopCounter, 10) Then '10 is column J
SheetArray(ArrayLoopCounter, 11) = SheetArray(ArrayLoopCounter, 10) 'set col K = Col J
Else
SheetArray(ArrayLoopCounter, 11) = SheetArray(ArrayLoopCounter, 4) 'set col K = Col D
End If
Else
SheetArray(ArrayLoopCounter, 11) = SheetArray(ArrayLoopCounter, 4) 'set col K = Col D
End If
Next ArrayLoopCounter
'Write the updated array back to the sheet
Worksheets(WorksheetToRead).Range(DataRange) = SheetArray
Finish = Timer
TotalTime = Finish - Start
MsgBox TotalTime
End Sub
I cannot test this right now, but i believe if you write a function to replace your nested IF statements, add it to Range("K2") with
Range("K2").Formula = ...
then copy it down to Cells(lastrow, "K"), copy all the functions and paste as values it'll be much faster.
Of course using
Application.Calculation = xlCalculationManual
and
Application.Calculation = xlCalculationAutomatic
like enemy suggested, along with
Application.screenupdate = false
Might make it slightly faster, but I think the function-copy-paste will make the biggest difference.
I don't have time to post updated code at the moment, but hopefully I'll get to it tomorrow.
Hope that helps!
EDIT: Here's the revised code
WARNING: I haven't been able to test this code yet. I'll do so tomorrow and revise if needed.
Sub FunctionCopyPaste()
Dim iLastRow as Integer
With Worksheets("Data")
iLastRow = .UsedRange.Cells(.UsedRange.Rows.Count,1).Row
.Range("K2").Formula = "=IF(J2<>0,IF(D2>J2,J2,D2),D2)"
.Range("K2").Copy Range(Cells(2,4), Cells(iLastRow,1).Row,4))
End With
With Range(Cells(2,4), Cells(iLastRow,4))
.Copy
.PasteSpecial xlPasteValues
End With
End Sub
I'm not sure if it will make a difference, but since you are timing it, I'd be interested to know.
I modified your code slightly. The main change is For each D in worksheets. Otherwise, I used Cells(row,col) instead of Range. Not that I expect that change to save time, I just thought you might like to see another way of defining cells, instead of concatenating letters and numbers.
note: with cells, you can use all variables, and numbers, with no letters. I just used letters to show you the similarities.
also, Since you have a c + 1 in every row, why not also just start on row 2, leave out the multiple (+1s) and go from there?
UN-TESTED
Sub itS1Capped()
Dim Start, Finish, TotalTime 'What are you declaring these variables as?
Dim c, d, j, lastRow
Start = Timer
'find how many rows
lastRow = Sheets("Data").Cells(Rows.Count, "A").End(xlUp).row
'loop through all rows
For c = 2 To lastRow 'c = IT S0 Uncapped (OLD d)
j = Sheets("Data").Cells(c, "J").Value 'IT Cap = Cells(c, 10)
If j <> 0 Then
If c > j Then
Sheets("Data").Cells(c, "K").Value = j 'IT S1 Capped = j
Else
Sheets("Data").Cells(c, "K").Value = c 'IT S1 Capped = c
End If
Else
Sheets("Data").Cells(c, "K").Value = c 'IT S1 Capped = c
End If
Next c
Finish = Timer
TotalTime = Finish - Start
MsgBox TotalTime
End Sub
edit: replaced d with c
Have you tried turning off automatic recalculation before you run your script?
Application.Calculation = xlCalculationManual
And then turn it back on when you're done?
Application.Calculation = xlCalculationAutomatic
That usually speeds up processing of lots of rows assuming you're not changing something that needs recalculating before you work on the next (or subsequent) rows.

Inefficient code that doesn't find matching data values

I have 3 issues with the following piece of code:
Intention of code: I have a table of data, 4 columns (F,G, H and I) wide and X rows long (X is typically between 5 and 400). I have a list of dates in column M, typically no more than 8 dates. Column H of table, contains dates as well. I want to find the dates that are in both columns (H and M) and whenever they appear, go to the same row in column I and set its value to zero, and the one after it (so if a match was in H100, then I100 and I101 would be zeroed).
issues with code: edited 1) as per feedback.
1) I have, using an if formula (=if(H100=M12,1,0), verified that there is one match, as how the spreadsheet sees it. The macro does not find this match, despite confirmation from the if formula. Cells I100 and I101 have nonzero values, when they should be zeroed.
2) the code runs, but takes about 3 minutes to go through 3 sheets of 180 rows of data. What can be done to make it run faster and more efficiently? It could have up to 30 sheets of data, and 400 rows (extreme example but possible, in this instance im happy to let it run a bit).
3) Assuming my data table before the macro is run, is 100 rows long, starting in row 12, after the macro, column I has nonzero values for 111 rows, and zeroes for the next 389. Is there a way I can prevent it from filling down zeroes, and leaving it blank?
I am using a correlate function afterwards on column I and there huge agreement of 0's with 0's is distorting this significantly. Thanks in advance,
Sub DeleteCells()
Dim ws As Worksheet
Dim cell As Range, search_cell As Range
Dim i As Long
Dim h As Long
Application.ScreenUpdating = False
For Each ws In ThisWorkbook.Worksheets
If Not ws.Name = "Cover" Then
For Each cell In ws.Range("H12:H500")
On Error Resume Next
h = ws.Range("G" & Rows.Count).End(xlUp).Row
i = ws.Range("L" & Rows.Count).End(xlUp).Row
Set search_cell = ws.Range("M12:M" & h).Find(what:=cell.Value, LookIn:=xlValues, lookat:=xlWhole)
On Error GoTo 0
If Not search_cell Is Nothing Then
ws.Range("I" & cell.Row).Value = 0
ws.Range("I" & cell.Row + 1).Value = 0
Set search_cell = Nothing
End If
Next cell
End If
Next ws
Application.ScreenUpdating = True
Set ws = Nothing: Set cell = Nothing: Set search_cell = Nothing
End Sub
EDIT: TESTED CODE, will work for 0, 1 row of data in H/M column starting from row 12?
EDIT: Updated the cell to handle case with 1 line of data, untested :|
I will give my solution first, this one should be much faster because it read the cells into memory first
Please comment if it doesn't work or you have further question
Sub DeleteCells()
Dim ws As Worksheet
Dim i As Long
Dim h As Long
Dim MColumn As Variant ' for convinence
Dim HColumn As Variant
Dim IColumn As Variant
Application.ScreenUpdating = False
For Each ws In ThisWorkbook.Worksheets
If Not ws.Name = "Cover" Then 'matching the target sheet
' matching the rows where column M's date matches column H's date
'starting row num is 12
With ws ' for simplifying the code
h = .Range("H" & .Rows.count).End(xlUp).Row
If h = 12 Then ' CASE for 1 row only
If Range("H12").Value = Range("M12").Value Then
Range("I12:I13").Value = ""
End If
ElseIf h < 12 Then
' do nothing
Else
ReDim HColumn(1 To h - 11, 1 To 1)
ReDim MColumn(1 To h - 11, 1 To 1)
ReDim IColumn(1 To h - 10, 1 To 1)
' copying the data from worksheet into 2D arrays
HColumn = .Range("H12:H" & h).Value
MColumn = .Range("M12:M" & h).Value
IColumn = .Range("I12:I" & h + 1).Value
For i = LBound(HColumn, 1) To UBound(HColumn, 1)
If Not IsEmpty(HColumn(i, 1)) And Not IsEmpty(MColumn(i, 1)) Then
If HColumn(i, 1) = MColumn(i, 1) Then
IColumn(i, 1) = ""
IColumn(i + 1, 1) = ""
End If
End If
Next i
'assigning back to worksheet cells
.Range("H12:H" & h).Value = HColumn
.Range("M12:M" & h).Value = MColumn
.Range("I12:I" & h + 1).Value = IColumn
End If
End With
End If
Next ws
Application.ScreenUpdating = True
End Sub