I apologize if already exist a similar question, but if yes, I not found.
I'm new to programming in VBA and still do not know much of it, now I'm trying to run a function that will verify if in a column "B" are repeated velores and if exist will check in a column "C" where the highest value, copying the lowest to another table and deleting it.
The code already does all this however need to run in tables of 65 000 lines and it takes a long time, never got for running these tables, because even when I run in tables with 5000 or 10000 lines takes approximately 6 to 15 minutes.
My question is if there is any way to optimize the cycle that I'm using, it will be better to use a For Each or maintain the Do While Loop?
Here is the code I am using:
Function Copy()
Worksheets("Sheet1").Range("A1:AQ1").Copy _
Destination:=Worksheets("Sheet2").Range("A1")
Dim lRow As Long
Dim lRow2 As Long
Dim Row As Long
Dim countA As Long
Dim countB As Long
Dim t As Double
lRow = 5000
Row = 2
countA = 0
countB = 0
Application.ScreenUpdating = False
ViewMode = ActiveWindow.View
ActiveWindow.View = xlNormalView
Application.EnableEvents = False
Application.DisplayStatusBar = False
ActiveSheet.DisplayPageBreaks = False
lRow2 = lRow - 1
t = Timer
Do While lRow > 2
If (Cells.Item(lRow, "B") <> Cells.Item(lRow2, "B")) Then
lRow = lRow - 1
lRow2 = lRow - 1
Else
If (Cells.Item(lRow, "C") > Cells.Item(lRow2, "C")) Then
Sheets("Sheet1").Rows(lRow2).Copy Sheets("Sheet2").Rows(Row)
Rows(lRow2).Delete
lRow = lRow - 1
Row = Row + 1
countA = countA + 1
Else
Sheets("Sheet1").Rows(lRow).Copy Sheets("Sheet2").Rows(Row)
Rows(lRow).Delete
lRow = lRow - 1
Row = Row + 1
countB = countB + 1
End If
lRow2 = lRow2 - 1
End If
Loop
Application.DisplayStatusBar = True
ActiveWindow.View = ViewMode
Application.ScreenUpdating = False
MsgBox "A = " & countA & " B = " & countB & "Time (minutes): " & (Timer - t) / 60
End Function
As long as you've entered the VBA environment for a solution, there seems little point in not continuing that avenue toward the best route possible. The following uses a pair of Scripting.Dictionaries to build two sets of data from the original matrix in Sheet1. In addition to the main sub procedure, there are two short 'helper' functions that breach the 65536 barrier that Application.Index and Application.Transpose suffer from. These are necessary to peel out a row from a large two-dimensioned array and flip the orientation of the results while simultaneously splitting the stored records.
Sub Keep_Highest_BC()
Dim d As Long, dHIGHs As Object, dDUPEs As Object
Dim v As Long, vTMPs() As Variant, iCOLs As Long
Debug.Print Timer
'On Error GoTo bm_Safe_Exit
Set dHIGHs = CreateObject("Scripting.Dictionary")
Set dDUPEs = CreateObject("Scripting.Dictionary")
With Worksheets("Sheet1")
iCOLs = .Columns("AQ").Column
.Cells(1, 1).Resize(2, iCOLs).Copy _
Destination:=Worksheets("Sheet2").Cells(1, 1)
With .Cells(2, 1).Resize(.Cells(Rows.Count, 2).End(xlUp).Row - 1, iCOLs)
vTMPs = .Value2
End With
End With
For v = LBound(vTMPs, 1) To UBound(vTMPs, 1)
If dHIGHs.exists(vTMPs(v, 2)) Then
If CDbl(Split(dHIGHs.Item(vTMPs(v, 2)), ChrW(8203))(2)) < vTMPs(v, 3) Then
dDUPEs.Add Key:=vTMPs(v, 2) & v, Item:=dHIGHs.Item(vTMPs(v, 2))
dHIGHs.Item(vTMPs(v, 2)) = joinAtoAQ(vTMPs, v)
Else
dDUPEs.Add Key:=vTMPs(v, 2) & v, Item:=joinAtoAQ(vTMPs, v)
End If
Else
dHIGHs.Add Key:=vTMPs(v, 2), Item:=joinAtoAQ(vTMPs, v)
End If
Next v
With Worksheets("Sheet1")
With .Cells(2, 1).Resize(.Cells(Rows.Count, 2).End(xlUp).Row - 1, iCOLs)
.ClearContents
With .Resize(dHIGHs.Count, iCOLs)
.Value = transposeSplitLargeItemArray(dHIGHs.items)
End With
End With
End With
With Worksheets("Sheet2")
With .Cells(1, 1).CurrentRegion.Offset(1, 0)
.ClearContents
With .Resize(dDUPEs.Count, iCOLs)
.Value = transposeSplitLargeItemArray(dDUPEs.items)
.Rows(1).Copy
.PasteSpecial Paste:=xlPasteFormats
Application.CutCopyMode = False
End With
End With
End With
bm_Safe_Exit:
dHIGHs.RemoveAll: Set dHIGHs = Nothing
dDUPEs.RemoveAll: Set dDUPEs = Nothing
Debug.Print Timer
End Sub
Function joinAtoAQ(vTMP As Variant, ndx As Long)
Dim sTMP As String, v As Long
For v = LBound(vTMP, 2) To UBound(vTMP, 2)
sTMP = sTMP & vTMP(ndx, v) & ChrW(8203)
Next v
joinAtoAQ = Left$(sTMP, Len(sTMP) - 1)
End Function
Function transposeSplitLargeItemArray(vITMs As Variant)
Dim v As Long, w As Long, vTMPs As Variant, vITM As Variant
ReDim vTMPs(LBound(vITMs) To UBound(vITMs), LBound(vITMs) To UBound(Split(vITMs(LBound(vITMs)), ChrW(8203))))
For v = LBound(vITMs) To UBound(vITMs)
vITM = Split(vITMs(v), ChrW(8203))
For w = LBound(vITM) To UBound(vITM)
vTMPs(v, w) = vITM(w)
Next w
Next v
transposeSplitLargeItemArray = vTMPs
End Function
Once the two dictionaries have been filled with maximum values and duplicate lesser values, the arrays are returned to the two worksheets en masse and subsequently split back into the 43 columns. One final effort is made to restore the original formatting from Sheet1 into Sheet2's data area.
I tested this on 75,000 rows of columns A through column AQ containing random sample data first with predominantly duplicate values in column B and then with roughly half duplicate values in column B. The first single pass was processed in 13.19 seconds; the second in 14.22. While your own results will depend on the machine you are running it on, I would expect a significant improvement over your original code. Post your own timed results (start and stop in seconds within the VBE's Immediate window, Ctrl+G) into the comments if you can.
Everything i could think of has already been mentioned above, however this code snippet might help someone out, it's the least you could do to make a macro faster (in case no interaction is required during runtime of the macro)
Run Optimize(True) at the start of your code, Optimize(False) at the end.
'Toggles unnecessary excel features
Sub Optimize(start As Boolean)
On Error Resume Next
With Application
.ScreenUpdating = Not (start)
.DisplayStatusBar = Not (start)
.EnableEvents = Not (start)
If start Then
.Calculation = xlCalculationManual
Else
.Calculation = xlCalculationAutomatic
End If
End With
On Error GoTo 0
End Sub
Typically it's faster to perform a single delete at the end of the loop.
Untested:
Function Copy()
Dim shtSrc As Worksheet, shtDest As Worksheet
Dim lRow As Long, Row As Long, viewmode
Dim countA As Long, countB As Long
Dim t As Double, rw As Range, rngDel As Range
lRow = 5000
Row = 2
countA = 0
countB = 0
Set shtSrc = Worksheets("Sheet1")
Set shtDest = Worksheets("Sheet2")
shtSrc.Range("A1:AQ1").Copy Destination:=shtDest.Range("A1")
Application.ScreenUpdating = False
viewmode = ActiveWindow.View
ActiveWindow.View = xlNormalView
Application.EnableEvents = False
Application.DisplayStatusBar = False
ActiveSheet.DisplayPageBreaks = False
t = Timer
Do While lRow > 2
Set rw = shtSrc.Rows(lRow)
If (rw.Cells(2) = rw.Cells(2).Offset(-1, 0)) Then
If (rw.Cells(3) > rw.Cells(3).Offset(-1, 0)) Then
rw.Offset(-1, 0).Copy shtDest.Rows(Row)
AddToRange rngDel, rw.Offset(-1, 0)
countA = countA + 1
Else
rw.Copy shtDest.Rows(Row)
AddToRange rngDel, rw
countB = countB + 1
End If
Row = Row + 1
End If
lRow = lRow - 1
Loop
'anything to delete?
If Not rngDel Is Nothing Then
rngDel.Delete
End If
Application.DisplayStatusBar = True
ActiveWindow.View = viewmode
Application.ScreenUpdating = False
MsgBox "A = " & countA & " B = " & countB & "Time (minutes): " & (Timer - t) / 60
End Function
'utility sub for building up a range
Sub AddToRange(rngTot, rng)
If rngTot Is Nothing Then
Set rngTot = rng
Else
Set rngTot = Application.Union(rng, rngTot)
End If
End Sub
Related
I'm looking for a way to speed up this code as it takes my computer 20-30 minutes to run. It essentially runs through a list of column values in sheet "A" and if It matches a column value in sheet "B" it will pull the entire corresponding row to the sheet "Match".
Sub MatchSheets()
Dim lastRowAF As Integer
Dim lastRowL As Integer
Dim lastRowM As Integer
Dim foundTrue As Boolean
Application.ScreenUpdating = False
lastRowAF = Sheets("FHA").Cells(Sheets("FHA").Rows.Count, "AF").End(xlUp).Row
lastRowL = Sheets("New Construction").Cells(Sheets("New Construction").Rows.Count, "L").End(xlUp).Row
lastRowM = Sheets("Match").Cells(Sheets("Match").Rows.Count, "A").End(xlUp).Row
For i = 1 To lastRowAF
foundTrue = False
For j = 1 To lastRowL
If Sheets("FHA").Cells(i, 32).Value = Sheets("New Construction").Cells(j, 12).Value Then
foundTrue = True
Exit For
End If
Next j
If foundTrue Then
Sheets("FHA").Rows(i).Copy Destination:= _
Sheets("Match").Rows(lastRowM + 1)
lastRowM = lastRowM + 1
End If
Next i
Application.ScreenUpdating = True
End Sub
Collections are optimized for looking values. Using a combination of a Collection and Array is usually the best way to match two list. 20K Rows X 54 Columns (140K Values) took this code 10.87 seconds to copy over on a slow PC.
Sub NewMatchSheets()
Dim t As Double: t = Timer
Const NUM_FHA_COLUMNS As Long = 54, AF As Long = 32
Dim list As Object
Dim key As Variant, data() As Variant, results() As Variant
Dim c As Long, r As Long, count As Long
ReDim results(1 To 50000, 1 To 100)
Set list = CreateObject("System.Collections.ArrayList")
With ThisWorkbook.Worksheets("New Construction")
data = .Range("L1", .Cells(.Rows.count, "L").End(xlUp)).Value
For Each key In data
If key <> "" Then
If Not list.Contains(key) Then list.Add key
End If
Next
End With
With ThisWorkbook.Worksheets("FHA")
data = .Range(.Range("A1").Resize(1, NUM_FHA_COLUMNS), .Cells(.Rows.count, AF).End(xlUp)).Value
For r = 1 To UBound(data)
key = data(r, AF)
If list.Contains(key) Then
count = count + 1
For c = 1 To UBound(data, 2)
results(count, c) = data(r, c)
Next
End If
Next
End With
If count = 0 Then Exit Sub
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = True
With ThisWorkbook.Worksheets("Match")
With .Cells(.Rows.count, "A").End(xlUp)
.Offset(1).Resize(count, NUM_FHA_COLUMNS).Value = results
End With
End With
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = False
Debug.Print Round(Timer - t, 2)
End Sub
use variant arrays:
Sub MatchSheets()
Dim lastRowAF As Long
Dim lastRowL As Long
Dim lastRowM As Long
Application.ScreenUpdating = False
lastRowAF = Sheets("FHA").Cells(Sheets("FHA").Rows.Count, "AF").End(xlUp).Row
lastRowL = Sheets("New Construction").Cells(Sheets("New Construction").Rows.Count, "L").End(xlUp).Row
lastRowM = Sheets("Match").Cells(Sheets("Match").Rows.Count, "A").End(xlUp).Row
Dim FHAArr As Variant
FHAArr = Sheets("FHA").Range(Sheets("FHA").Cells(1, 1), Sheets("FHA").Cells(lastRowAF, Columns.Count).End(xlToLeft)).Value
Dim NewConArr As Variant
NewConArr = Sheets("New Construction").Range(Sheets("New Construction").Cells(1, 12), Sheets("New Construction").Cells(lastRowL, 12)).Value
Dim outarr As Variant
ReDim outarr(1 To UBound(FHAArr, 1), 1 To UBound(FHAArr, 2))
Dim k As Long
k = 0
Dim l As Long
For i = 1 To lastRowAF
For j = 1 To lastRowL
If FHAArr(i, 32) = NewConArr(j, 1) Then
For l = 1 To UBound(FHAArr, 2)
k = k + 1
outarr(k, l) = FHAArr(i, l)
Next l
Exit For
End If
Next j
Next i
Sheets("Match").Cells(lastRowM + 1, 1).Resize(UBound(outarr, 1), UBound(outarr, 2)).Value = outarr
Application.ScreenUpdating = True
End Sub
FHA Worksheet: 2500 rows by 50 columnsNew Construction Worksheet: 500 rows by 1 column LMatch Worksheet: 450 transfers from FMA Elapsed time: 0.13 seconds
Get rid of all the nested loop and work with arrays.
Your narrative seemed to suggest that there might be multiple matches for any one value but your code only looks for a single match then Exit For. I'll work with the latter of the two scenarios.
Sub MatchSheets()
Dim i As Long, j As Long
Dim vFM As Variant, vNC As Variant
Debug.Print Timer
With Worksheets("New Construction")
vNC = .Range(.Cells(1, "L"), _
.Cells(.Rows.Count, "L").End(xlUp)).Value2
End With
With Worksheets("FHA")
vFM = .Range(.Cells(1, "A"), _
.Cells(.Rows.Count, _
.Cells(1, .Columns.Count).End(xlToLeft).Column).End(xlUp)).Value2
End With
ReDim vM(LBound(vFM, 2) To UBound(vFM, 2), 1 To 1)
For i = LBound(vFM, 1) To UBound(vFM, 1)
If Not IsError(Application.Match(vFM(i, 32), vNC, 0)) Then
For j = LBound(vFM, 2) To UBound(vFM, 2)
vM(j, UBound(vM, 2)) = vFM(i, j)
Next j
ReDim Preserve vM(LBound(vFM, 2) To UBound(vFM, 2), LBound(vM, 2) To UBound(vM, 2) + 1)
End If
Next i
With Worksheets("match")
.Cells(.Rows.Count, "A").End(xlUp).Offset(1, 0).Resize(UBound(vM, 2), UBound(vM, 1)) = _
Application.Transpose(vM)
End With
Application.ScreenUpdating = True
Debug.Print Timer
End Sub
Try changing this line:
Sheets("FHA").Rows(i).Copy Destination:= _
Sheets("Match").Rows(lastRowM + 1)
For the following line:
Sheets("Match").Rows(lastRowM + 1).Value for Sheets("FHA").Rows(i).value
If you really need to shave milliseconds, you could also set: lastRowM to be:
lastRowM = Sheets("Match").Cells(Sheets("Match").Rows.Count, "A").End(xlUp).Row + 1
And use:
Sheets("Match").Rows(lastRowM).Value for Sheets("FHA").Rows(i).value
Thus saving you an addition every time you go through that part of the code
I am trying to take a range of cells (column B to be specific) and find the cells in that range that have a value less than zero and clear the contents of those cells. Is there a way to do this without looping through every single cell? The column is a very large data set that gets longer each week so looping takes a significant amount of time.
Below is the current loop I am using
Dim sht As Worksheet
Dim LastColumn As Long
Set sht = ThisWorkbook.Worksheets("Sheet1")
lastrow = sht.Cells.Find("*", SearchOrder:=xlByRows,
searchdirection:=xlPrevious).Row
for i=1 to lastrow
if sheets("time").cells(i, "B") then
sheets("time").cells(i, "B").clear
end if
next i
The cells I am trying to examine and then potentially delete contain formulas
edit: The answer marked as accepted sped up the process but still requires a loop. If anyone has anything that would be faster than what is posted feel free to add it.
As per my comment. I run this on 50k rows, took minor amounts of time.
Option Explicit
Sub update_column()
Dim Column_to_run_on As String
Dim LR As Long, i As Long
Dim arr As Variant
'change as needed
Column_to_run_on = "D"
'change sheet as needed
With Sheets("Sheet1")
LR = .Range(Column_to_run_on & "1048575").End(xlUp).Row
'"2:" here as I assume you have a header row so need to start from row 2
arr = .Range(Column_to_run_on & "2:" & Column_to_run_on & LR)
For i = 1 To UBound(arr, 1)
If arr(i, 1) < 0 Then
arr(i, 1) = 0
End If
Next
.Range(Column_to_run_on & "2:" & Column_to_run_on & LR).Value = arr
End With
End Sub
No loop is needed. Say we have data in B1 through B21 like:
This tiny macro:
Sub RemoveNegs()
With Range("B1:B21")
.Value = Evaluate("IF(" & .Address & " < 0,""""," & .Address & ")")
End With
End Sub
will produce:
Not appropriate if the cells contain formulas.
i tested lopps with vba array against both solutions, loops is at least 2 to 5 times faster in each case:
Option Explicit
Sub fill()
Dim t As Double
t = Timer
Dim x&
Dim y&
Dim arr()
With Application
.ScreenUpdating = False
.EnableEvents = False
.Calculation = xlCalculationManual
ReDim arr(1 To 2000, 1 To 1000)
For x = 1 To 1000
For y = 1 To 2000
arr(y, x) = Rnd() * 1111 - 555
Next y
Next x
Range(Cells(1, 1), Cells(2000, 1000)).Value2 = arr
.ScreenUpdating = True
.Calculation = xlCalculationAutomatic
.EnableEvents = True
Debug.Print Timer - t
End With
Erase arr
End Sub
Sub nega()
Dim t As Double
t = Timer
Dim x&
Dim y&
Dim arr()
With Application
.ScreenUpdating = False
.EnableEvents = False
.Calculation = xlCalculationManual
'With Range("A1", Cells(2000, 1000))
' .Value2 = Evaluate("if(" & .Address & " <0,""""," & .Address & ")")
'End With
'Range(Cells(1, 1), Cells(2000, 1000)).Replace "-*", ""
arr = Range(Cells(1, 1), Cells(2000, 1000)).Value2
For x = 1 To 1000
For y = 1 To 2000
If arr(y, x) < 0 Then arr(y, x) = vbNullString
Next y
Next x
Range(Cells(1, 1), Cells(2000, 1000)).Value2 = arr
.ScreenUpdating = True
.Calculation = xlCalculationAutomatic
.EnableEvents = True
End With
Erase arr
Debug.Print Timer - t
End Sub
I can not manage to cleanse my data of the "empty" rows. There is no problem in deleting the "0" but those cells which are empty are not empty but have something like "null strings" in it.
Sub Reinigung()
Application.ScreenUpdating = False
Application.EnableEvents = False
ListeEnde3 = ThisWorkbook.Sheets("input").Cells(Rows.Count, 1).End(xlUp).Row
For Zeile1 = 2 To ListeEnde3
If ThisWorkbook.Sheets("input").Cells(Zeile1, 14) = "0" Or ThisWorkbook.Sheets("2018").Cells(Zeile1, 14) = "" Then
ThisWorkbook.Sheets("input").Rows(Zeile1).Delete
Zeile1 = Zeile1 - 1
Else
End If
Next
' ThisWorkbook.Sheets("import").Columns(14).SpecialCells(xlCellTypeBlanks).EntireRow.Delete
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
That code just freezes my excel, if i leave out the
thisWorkbook.Sheets("2018").Cells(Zeile1, 14) = ""
part, it works and deletes all rows, where colum 14 contains a "0".
If I check the cells which appear blank with =isblank it returns "false". There is no "space" in the cell and no " ' ".
What to do?
edit
After the first tips my code looks like this now:
Sub Reinigung()
Dim ListeEnde3 As Long
Dim Zeile1 As Long
Application.ScreenUpdating = False
Application.EnableEvents = False
ListeEnde3 = ThisWorkbook.Sheets("import").Cells(Rows.Count, 1).End(xlUp).Row
For Zeile1 = ListeEnde3 To 2 Step -1
Set rngX = ThisWorkbook.Sheets("import").Cells(Zeile1, 14)
If (rngX = "0" Or rngX = "") Then 'or rngY = vbNullString
ThisWorkbook.Sheets("import").Rows(Zeile1).Delete
End If
Next Zeile1
' ThisWorkbook.Sheets("import").Columns(14).SpecialCells(xlCellTypeBlanks).EntireRow.Delete
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
Excel still crashes / freezes (I waited for 5 minutes) but since the code runs "smoothly" with F8 I wanted to give it a shot with less data: It works!
If I am not reducing the data there are ~ 70000 rows to check. I let it run on 720 rows and it worked.
Any way to tweak the code in a way that it can handle the 70000+ rows? I didn't think that it would be too much.
Thanks!
You can use AutoFilter and delete the visible rows (not tested) :
Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("import")
ws.UsedRange.AutoFilter 14, Array("=0", "="), xlFilterValues
ws.UsedRange.Offset(1).EntireRow.Delete
ws.AutoFilterMode = False
Another way is to simply use internal arrays and write out the new data set which has valid rows.
It is very fast.
If your dataset has formulas then you'll have to use extra code, but if it's constants only, then the below should do:
Sub Reinigung()
'Here I test with column E to Z, set Ranges appropriately
Application.ScreenUpdating = False
Application.EnableEvents = False
Dim ListeEnde3 As Long, x As Long, y As Long
'last row of data - set to column of non-blank data
ListeEnde3 = ThisWorkbook.Sheets("import").Cells(Rows.Count, 5).End(xlUp).Row
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets("import")
Dim startCell As Range
'set to whatever cell is the upper left corner of data
Set startCell = ThisWorkbook.Sheets("import").Range("E1")
Dim arr As Variant, arrToPrint() As Variant
'Get rightmost column of data instead of hardcoding to "Z"
'write dataset into an array
arr = ws.Range(startCell, ws.Range("Z" & ListeEnde3)).Value
x = UBound(arr) - LBound(arr) + 1 'num of rows of data
y = UBound(arr, 2) - LBound(arr, 2) + 1 'num of columns of data
ReDim arrToPrint(1 To x, 1 To y) 'array to hold valid/undeleted data
Dim i As Long, j As Long, printCounter As Long, arrayColumnToCheck as Long
arrayColumnToCheck = 14 - startCell.Column + 1 '14 is column N
For i = 1 To x
If arr(i, arrayColumnToCheck ) <> 0 And arr(i, arrayColumnToCheck ) <> vbNullString Then
printCounter = printCounter + 1
For j = 1 To y
'put rows to keep in arrToPrint
arrToPrint(printCounter, j) = arr(i, j)
Next j
End If
Next i
'Print valid rows to keep - only values will print - no formulas
startCell.Resize(printCounter, y).Value = arrToPrint
'Delete the rows with zero & empty cells off the sheet
startCell.Offset(printCounter).Resize(ListeEnde3 - printCounter, y).Delete xlShiftUp
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
You can add IsEmpty to your code to check the cells filling
Sub Reinigung()
Application.ScreenUpdating = False
Application.EnableEvents = False
ListeEnde3 = ThisWorkbook.Sheets("input").Cells(Rows.Count, 1).End(xlUp).Row
For Zeile1 = 2 To ListeEnde3
Set rngX = ThisWorkbook.Sheets("input").Cells(Zeile1, 14)
Set rngY = ThisWorkbook.Sheets("2018").Cells(Zeile1, 14)
If (rngX = "0" And (Not IsEmpty(rngX))) Or (rngY = "") Then
ThisWorkbook.Sheets("input").Rows(Zeile1).Delete
Zeile1 = Zeile1 - 1
End If
Next
' ThisWorkbook.Sheets("import").Columns(14).SpecialCells(xlCellTypeBlanks).EntireRow.Delete
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
NEVER a good idea to alter a loop counter: Zeile1 = Zeile1 - 1
Instead start at the end and use Step -1 in your loop to work backward.
You are in a infinite loop because the loop doesnt move forward. If Zeile=3 and there is a "" in row3 in the '2018' sheet, then it will always be stuck on the Zeile1 = 3 line. You will always be coming back to that "" on row 3 in '2018'sheet.
For Zeile1 = ListeEnde3 To 2 Step -1
Set rngX = ThisWorkbook.Sheets("input").Cells(Zeile1, 14)
Set rngY = ThisWorkbook.Sheets("2018").Cells(Zeile1, 14)
If (rngX = "0" Or rngY = "") Then 'or rngY = vbNullString
ThisWorkbook.Sheets("input").Rows(Zeile1).Delete
End If
Next Zeile1
Here is my problem. I managed to create a macro that looks like this:
Sub Macro1()
Range("G17:G36").Select
ActiveSheet.Shapes.AddChart.Select
ActiveChart.SetSourceData Source:=Range("'Sheet1'!$G$17:$G$36")
ActiveChart.ChartType = xlLine
End Sub
I know this was pretty basic to record but my problem is how to change it and make the range dynamic and conditional. For example when I get to the row 17 I have a value in the cell D17 that is greater than lets say 200 and a value in E17 greater than 100. This should trigger the beginning of my range. So if D17>200 AND E17>100 I need to get G17 as the beginning of the range. As for G36 (the end of the range) the logic is very similar but this time I would test for a condition like this: IF F36<64 THEN get G36 as the end of the range.
The should repeat till the end. For example the last row could be at 28000 so I expect a good few of these charts to be created along the way.
Thanks is advance for your help,
Schroedinger.
This is how it looks now and gives me a run-time error explained in my correspondence with EngJon.
Sub GenerateCharts()
Application.ScreenUpdating = False
'Get the last row
Dim LastRow As Long
LastRow = ActiveSheet.UsedRange.Rows.Count
Dim endOfRange As Long
Dim wholeRange As Range
Dim i As Long
For i = 1 To LastRow
If Cells(i, 4) > 0.000001 And Cells(i, 5) > 0.00000002 Then
'Determine the end of the range
endOfRange = DetermineRange(i)
Set wholeRange = Range(Cells(i, 7), Cells(endOfRange, 7))
NewChart (wholeRange)
i = endOfRange
End If
Next i
Application.ScreenUpdating = True
End Sub
Function DetermineRange(row As Long) As Long
Dim LastRow As Long
LastRow = ActiveSheet.UsedRange.Rows.Count
Dim j As Long
For j = row To LastRow
If Cells(j, 6) < -0.0000000018 Then
DetermineRange = j
Exit Function
End If
Next j
DetermineRange = j
End Function
Function NewChart(rng As Range)
ActiveSheet.Shapes.AddChart.Select
ActiveChart.SetSourceData Source:=rng
ActiveChart.ChartType = xlLine
End Function
This is a final solution for me. I hope it helps someone. Big tnx to EngJon and Paagua Grant.
Sub GenerateCharts()
Application.ScreenUpdating = False
Dim StartCell As Long
Dim EndCell As Long
Dim ChartRange As Range
Dim DataEnd As Long
Dim i As Integer
Dim j As Integer
Dim HasStart As Boolean
Dim HasEnd As Boolean
'Sets end of data based on the row you are charting
DataEnd = Cells(Rows.Count, 7).End(xlUp).Row
'Begin loop to find start and end ranges, create charts based on those ranges
For i = 1 To DataEnd
If HasStart Then
If Cells(i, 4).Value < 0 Then
EndCell = i
HasEnd = True
End If
Else 'If there isn't a starting cell yet
If Cells(i, 4).Value > 0.000001 And Cells(i, 5).Value > 0.00000002 Then
StartCell = i
HasStart = True
End If
End If
If HasStart And HasEnd Then
Set ChartRange = ActiveSheet.Range(Cells(StartCell, 7), Cells(EndCell, 7))
ActiveSheet.Shapes.AddChart(xlLine, _
Left:=ActiveSheet.Range(Cells(StartCell, 10), Cells(StartCell, 10)).Left, _
Top:=ActiveSheet.Range(Cells(StartCell, 10), Cells(StartCell, 10)).Top, _
Width:=ActiveSheet.Range(Cells(StartCell, 10), Cells(StartCell, 20)).Width, _
Height:=ActiveSheet.Range(Cells(StartCell, 10), Cells(StartCell + 25, 10)).Height _
).Select
ActiveChart.SetSourceData Source:=ChartRange
HasStart = False
HasEnd = False
End If
Next
Application.ScreenUpdating = True
End Sub
You can use your recorded Macro1 as a Function and call it when you need to create a new Chart:
Function NewChart(rng As Range)
ActiveSheet.Shapes.AddChart.Select
ActiveChart.SetSourceData Source:=rng
ActiveChart.ChartType = xlLine
End Function
You will also need the following function:
Function DetermineRange(row As Long) As Long
Dim LastRow As Long
LastRow = ActiveSheet.UsedRange.Rows.Count
Dim j As Long
For j = row To LastRow
If Cells(j, 6) < 64 Then
DetermineRange = j
Exit Function
End If
Next j
DetermineRange = j
End Function
You will call it in a Sub that iterates over all rows:
Sub GenerateCharts()
Application.ScreenUpdating = False
'Get the last row
Dim LastRow As Long
LastRow = ActiveSheet.UsedRange.Rows.Count
Dim endOfRange As Long
Dim wholeRange As Range
Dim i As Long
For i = 1 To LastRow
If Cells(i, 4) > 200 And Cells(i, 5) > 100 Then
'Determine the end of the range
endOfRange = DetermineRange(i)
Set wholeRange = Range(Cells(i, 7), Cells(endOfRange, 7))
NewChart wholeRange
i = endOfRange
End If
Next i
Application.ScreenUpdating = True
End Sub
Copy those three in a module and execute the Sub. Please comment if this did what you needed.
Here's a slightly different option that performs all of the tasks in a single function.
Option Explicit
Private Sub CommandButton1_Click()
Application.ScreenUpdating = False
Dim StartCell As Long, EndCell As Long, ChartRange As Range, DataEnd As Long, i As Integer, j As Integer, HasStart As Boolean, HasEnd As Boolean, _
ChartTop As Long, ChartHeight As Long
DataEnd = Cells(Rows.Count, 7).End(xlUp).Row 'Sets end of data based on the row you are charting.
ChartTop = 50
ChartHeight = 100
'Begin loop to find start and end ranges, create charts based on those ranges.
For i = 1 To DataEnd
If HasStart Then
If Cells(i, 6).Value < 64 Then
EndCell = i
HasEnd = True
End If
Else 'If there isn't a starting cell yet.
If Cells(i, 7).Value > 200 And Cells(i, 5).Value > 100 Then
StartCell = i
HasStart = True
End If
End If
If HasStart And HasEnd Then
Set ChartRange = ActiveSheet.Range(Cells(StartCell, 7), Cells(EndCell, 7))
ActiveSheet.Shapes.AddChart(Top:=ChartTop, Height:=ChartHeight).Select
With ActiveChart
.SetSourceData Source:=ChartRange
.ChartType = xlLine
End With
ChartTop = ChartTop + ChartHeight + 15
HasStart = False
HasEnd = False
End If
Next
Application.ScreenUpdating = True
End Sub
This also makes sure that each chart created by the tool does not overlap the previous chart.
For the sake of space and clarity, I am putting my response to your followup questions here.
Assuming standard row heights and column widths, you can set
ChartTop =(StartCell-1)*15
to set the top of the chart to begin at the top of the same row as your data, and within the
ActiveSheet.Shapes.AddChart(Top:=ChartTop, Height:=ChartHeight).Select
you can add
Left:=(X * 48)
where X is one less than the column number that you want the chart to be left-aligned to, e.g. if you want the chart to start at the left edge of Column I, X would be equal to 8. However, as far as I can tell, there is no easy way to adjust these values if your row height/column widths is non-standard, e.g. if you have auto-fit your columns to your data.
I have the following problem to solve.
I have an excel sheet with 3 columns and 29000 rows.
Column a is an index number.
Column b is an id number.
Column c is a number which points to an index of column a
So if column c is 200. I need to go to column a 200 and take it's column b id and put it on the same row as the column c index.
The purpose of this is to link the id number of two items, who are linked by this column c.
(I hope I am making sense :/ )
So I have been trying to code this in VBA. At the moment I am using a nested for loop, but as you can imagine, the run time is pretty long....
dim i as integer
dim v as integer
dim temp as integer
i = 1
v=1
for i = 1 to 29000
if cells(i,3).value > 0 then
temp = cells(i,3).Value
cells(i,5).value = cells(1,2).value
for v = 1 to 29000
if cells(v,1).value = temp and cells(i,5).value <> cells(v,2).value then
cells(i,6).value = cells(v,2).value
end if
next
end if
next
So it does work and performs what I want, but the run time is just too long. Any ideas how to streamline the program?
I am pretty new to vba and programming in general.
Thanks in advance
Untested, but compiled OK
Sub Test()
Dim dict As Object
Dim i As Long
Dim temp As Long
Dim sht As Worksheet
Dim oldcalc
Set sht = ActiveSheet
Set dict = GetMap(sht.Range("A1:B29000"))
With Application
.ScreenUpdating = False
oldcalc = .Calculation
.Calculation = xlCalculationManual
End With
For i = 1 To 29000
If Cells(i, 3).Value > 0 Then
temp = Cells(i, 3).Value
Cells(i, 5).Value = Cells(1, 2).Value
If dict.exists(temp) Then
If sht.Cells(i, 5).Value <> dict(temp) Then
sht.Cells(i, 6).Value = dict(temp)
End If
End If
End If
Next
With Application
.ScreenUpdating = True
.Calculation = oldcalc 'restore previous setting
End With
End Sub
Function GetMap(rng As Range) As Object
Dim rv As Object, arr, r As Long, numRows As Long
Set rv = CreateObject("scripting.dictionary") 'EDITED to add Set
arr = rng.Value
numRows = UBound(arr, 1)
For r = 1 To numRows
If Not rv.exists(arr(r, 1)) Then
rv.Add arr(r, 1), arr(r, 2)
End If
Next r
Set GetMap = rv
End Function