This question already has answers here:
Improving the performance of FOR loop
(3 answers)
Closed 7 years ago.
Trying to use Union and ranges to speed up deleting empty columns, across all sheets in my workbook, except "AA" and "Word Frequency"
Sample workbook
Example of sheet before:
Example of sheet after (note, I will need to write separate script to shift keywords up, you can't see all the keywords, but only the columns with data in them are left):
In my search for a method to speed up deleting columns in a sheet if the column is empty (except the header), I was directed by #chrisneilsen to reference the thread Improving the performance of FOR loop.
That thread shed light on the fact that deleting columns individually slows down performance significantly. Script speed can be improved by defining a "master" range to include all the ranges (columns) to be deleted (by using Union), and then simply deleting the "master" range.
As a VBA noob, I used to following references to learn about Ranges, Union, UBound and LBound to understand the code in the thread mentioned above:
Excel-Easy.com: Using UBound and LBound, Dynamic Arrays (Using ReDim)
Youtube: Using UNION method to select (and modify) multiple ranges
My old (slow) script that works, but takes about about 3 hours to run across ~30 sheets and deleting ~100 columns each sheet:
Sub Delete_No_Data_Columns()
Dim col As Long
Dim h 'to store the last columns/header
h = Range("E1").End(xlToRight).Column 'find the last column with the data/header
Application.ScreenUpdating = False
For col = h To 5 Step -1
If Application.CountA(Columns(col)) = 1 Then Columns(col).Delete
Next col
End Sub
Almost working script (for one sheet), using the same approach as #chrisneilsen code in thread mentioned above. When I run it, it doesn't do anything, however #chrisneilsen noted there were 2 syntax errors (Column. instead of Columns.) and that I was mixing an implicit ActiveSheet (by using Columns without a qualifier) with an explicit sheet Worksheets("Ball Shaker"). Errors in code are commented below.
Sub Delete_No_Data_Columns_Optimized()
Dim col As Long
Dim h 'to store the last columns/header
Dim EventState As Boolean, CalcState As XlCalculation, PageBreakState As Boolean
Dim columnsToDelete As Range
Dim ws as Worksheet '<<<<<<<<< Fixing Error (qualifying "Columns." properly)
On Error GoTo EH:
'Optimize Performance
Application.ScreenUpdating = False
EventState = Application.EnableEvents
Application.EnableEvents = False
CalcState = Application.Calculation
Application.Calculation = xlCalculationManual
PageBreakState = ActiveSheet.DisplayPageBreaks
ActiveSheet.DisplayPageBreaks = False
' <<<<<<<<<<<<< MAIN CODE >>>>>>>>>>>>>>
Set ws = ActiveSheet
h = Range("E1").End(xlToRight).Column 'find the last column with the data/header
'<<<<<<<<<<<<<< Errors corrected below in comments >>>>>>>>>>>>
For col = h To 5 Step -1
If Application.CountA(Column(col)) = 1 Then
'<<<<< should be Application.CountA(ws.Columns(col)) = 1
If columnsToDelete Is Nothing Then
Set columnsToDelete = Worksheets("Ball Shaker").Column(col)
'should be columnsToDelete = ws.Columns(col)
Else
Set columnsToDelete = Application.Union(columnsToDelete, Worksheets("Ball Shaker").Column(col))
'should be columnsToDelete = Application.Union(columnsToDelete, ws.Columns(col))
End If
End If
Next col
'<<<<<<<<<<<<<< End Errors >>>>>>>>>>>>>>>>
If Not columnsToDelete Is Nothing Then
columnsToDelete.Delete
End If
' <<<<<<<<<<<< END MAIN CODE >>>>>>>>>>>>>>
CleanUp:
'Revert optmizing lines
On Error Resume Next
ActiveSheet.DisplayPageBreaks = PageBreakState
Application.Calculation = CalcState
Application.EnableEvents = EventState
Application.ScreenUpdating = True
Exit Sub
EH:
' Handle Errors here
Resume CleanUp
End Sub
Working code that runs across all sheets in workbook, in about ~6 minutes (except "AA" and "Word Frequency" worksheets, which I don't need to format):
Option Explicit
Sub Delete_No_Data_Columns_Optimized_AllSheets()
Dim sht As Worksheet
For Each sht In Worksheets
If sht.Name <> "AA" And sht.Name <> "Word Frequency" Then
sht.Activate 'go to that Sheet!
Delete_No_Data_Columns_Optimized sht.Index 'run the code, and pass the sht.Index _
'of the current sheet to select that sheet
End If
Next sht 'next sheet please!
End Sub
Sub Delete_No_Data_Columns_Optimized(shtIndex As Integer)
Dim col As Long
Dim h 'to store the last columns/header
Dim EventState As Boolean, CalcState As XlCalculation, PageBreakState As Boolean
Dim columnsToDelete As Range
Dim ws As Worksheet
Set ws = Sheets(shtIndex) 'Set the exact sheet, not just the one that is active _
'and then you will go through all the sheets
On Error GoTo EH:
'Optimize Performance
Application.ScreenUpdating = False
EventState = Application.EnableEvents
Application.EnableEvents = False
CalcState = Application.Calculation
Application.Calculation = xlCalculationManual
PageBreakState = ActiveSheet.DisplayPageBreaks
ActiveSheet.DisplayPageBreaks = False
' <<<<<<<<<<<<< MAIN CODE >>>>>>>>>>>>>>
h = ws.Range("E1").End(xlToRight).Column 'find the last column with the data/header
For col = h To 5 Step -1
If ws.Application.CountA(Columns(col)) = 1 Then 'Columns(col).Delete
If columnsToDelete Is Nothing Then
Set columnsToDelete = ws.Columns(col)
Else
Set columnsToDelete = Application.Union(columnsToDelete, ws.Columns(col))
End If
End If
Next col
If Not columnsToDelete Is Nothing Then
columnsToDelete.Delete
End If
' <<<<<<<<<<<< END MAIN CODE >>>>>>>>>>>>>>
CleanUp:
'Revert optmizing lines
On Error Resume Next
ActiveSheet.DisplayPageBreaks = PageBreakState
Application.Calculation = CalcState
Application.EnableEvents = EventState
Application.ScreenUpdating = True
Exit Sub
EH:
' Handle Errors here
Resume CleanUp
End Sub
Note: Trying to delete columns and shift to left, so columns with data inside will all be grouped together neatly after script is run.
Is this the best way to utilize Union and ranges for deleting columns? Any help would be greatly appreciated.
The special cells method actually will not serve you so well here. Instead, find the last row of data in your sheet and delete only the cells in the column up to the that row and shift everything to the left. This will be much faster than deleting an entire column!
Sub Delete_No_Data_Columns()
Dim col As Long, lRow as Long
Dim h as Long'to store the last columns/header
lRow = Range("E" & Rows.Count).End(xlUp).Row ' assumes column E will have last used row ... adjust as needed
h = Range("E1").End(xlToRight).Column 'find the last column with the data/header
For col = h To 5 Step -1
If Application.CountA(Columns(col)) = 1 Then
Range(Cells(2,col),Cells(lRow,col)).Delete shift:=xlToLeft
End If
Next col
Application.ScreenUpdating = False ' i think you want this at the beginning of the program, no?
End Sub
Related
I am trying to clear the contents of all cells on a worksheet apart from a specific range. I have tried to copy the range to the clipboard then to paste it back on again in the same place, however excel being the usual tricky beast - it doesn't want to play ball.
The range I would like to keep the same is AB1:AC5.
Any Suggestions Apprichiated...
(here is my code)
Sub Button21_Click()
Application.ScreenUpdating = False
With Worksheets(2)
.Range("AB1:AC5").Copy
.Cells.ClearContents
.Paste(Destination:=Sheets("Data").Range("AB1"))
End With
Application.ScreenUpdating = True
End Sub
use an array instead:
Sub Button21_Click()
Application.ScreenUpdating = False
Dim oldValues As Variant
With Worksheets(2)
oldValues = .Range("AB1:AC5").Value
.Cells.ClearContents
.Range("AB1:AC5").Value = oldValues
End With
Application.ScreenUpdating = True
End Sub
I did a program in VBA to copy the formulas in each cell in a specific column, I have 30501 points and the program is really slow even to calculate 100 points, there is a better way to do so?
Sub Copyformulas()
Dim i As Integer
Dim cell As Range
Dim referenceRange As Range
Dim a As String
a = "$T$30510"
Set range1= ActiveSheet.Range("A1:A30510")
Set myrange = Range("T16:T30510")
i = 16
Do Until Cells(20, 30510)
With range1
For Each cell In myrange
If cell.HasFormula Then
Cells(i, 35).Value = cell.Address
Cells(i, 36).Value = "'" & CStr(cell.Formula)
i = i + 1
End If
Next
End With
Loop
End Sub
You can use SpecialCells to refine your range. You don't need to use ActiveSheet it is implied.
Set rSource = Range("A16:A30510").SpecialCells(xlCellTypeFormulas)
Sub Copyformulas()
Application.Calculation = xlManual
Application.ScreenUpdating = False
Application.EnableEvents = False
Dim c As Range
Dim rSource As Range
Set rSource = ActiveSheet.Range("A16:A30510").SpecialCells(xlCellTypeFormulas)
For Each c In rSource
c.Offset(0, 34) = c.Address
c.Offset(0, 35) = "'" & c.Formula
Next
Application.Calculation = xlAutomatic
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
Try adding the following:
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
Application.EnableEvents = False
... Your Code ...
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
Application.EnableEvents = True
You may only need the first one, but they are all good practice in using. Also, where are you using the With ... End With statement? I don't see any use of it in the block.
It is good practice to use Option Explicit at the top of the module. And range1 and myrange are not declared.
Application.Calculation
When a worksheet is accessed or a range's precedents has changed, Excel will automatically recalculate the formulas on the worksheet. Since you are looping over 30,000 times, this causes Excel to recalculate each time through the loop and, thus, slows down performance.
Application.ScreenUpdating
This line stops Excel from screen flashes and other things that occur as the macro runs.
Application.EnableEvents
This line turns off events, such as Worksheet_Change, so that the event is not triggered. If it is not turned off then any time a change occurs on the worksheet the code in the change event will run. If you have a Worksheet_SelectionChange event then code will run every time you select a different cell. These events are written in the worksheet or workbook objects located in the project window of the VBE and there are many events to choose from. Here is a very simple illustration. Place the following in the Sheet1 object in the project window:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
MsgBox "Hi!"
End Sub
Now click around on the worksheet. You see it responds to each selection change. Now place the following in a regular module:
Sub TestEnableEvents()
Application.EnableEvents = False
ActiveCell.Offset(1, 0).Select
Application.EnableEvents = True
End Sub
When you run the above code the message box will not be triggered.
Background:
I want to hide columns in a sheet based on whether there is an x in row 7. The x is not typed in but filled in via a formula.
I used the following code in another worksheet, were it works. The only thing I changed is the name of the sub, the worksheet and the row (7 instead of 5).
However whenever I try to manually run this sub from the vba editor as a test, it produces a runtime error 13 (mismatched type).
Sub hidCol2()
Dim i As Long
Application.ScreenUpdating = False
Set ws = ThisWorkbook.Worksheets("Zeitplan")
ws.Cells.EntireColumn.Hidden = False
For i = Cells(7, Columns.Count).End(xlToLeft).Column To 1 Step -1
If Cells(7, i) = "x" Then Cells(7, i).EntireColumn.Hidden = True
Next i
Application.ScreenUpdating = True
End Sub
My Question:
Why does the above code produce a runtime error 13, what do I need to correct?
Here it is :
Note it works without Dim ws but I think it's a good practice to dimension the variables before use.
If anyone can let me know why Dim ws here wasn't necessary that would clear some doubts in my head.
Sub hidCol2()
Dim i As Long
Dim ws As Worksheet 'As Suggested by #eirikdaude but I don't know why it worked without it as well (Tested on a workbook with a single worksheet)
Application.ScreenUpdating = False
Set ws = ThisWorkbook.Worksheets("Zeitplan")
ws.Activate
ws.Cells.EntireColumn.Hidden = False
For i = ws.Cells(7, Columns.Count).End(xlToLeft).Column To 1 Step -1
If Trim(ws.Cells(7, i).Text) = "x" Then ws.Cells(7, i).EntireColumn.Hidden = True
Next i
Application.ScreenUpdating = True
End Sub
Disclaimer: I am relatively new to vba and macros.
I have written a macro to update value and formatting in some individual cells after reading and parsing a json through http and the process is very slow, so I broke down the code into different portions to see where the bottleneck might be. Turns out the cell updating is the problem, I have the following code:
Sub test()
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
Application.EnableEvents = False
Application.EnableCancelKey = False
t = Timer
With Range("A1")
.Font.Italic = True
.Interior.ColorIndex = 37
.Value = 3412
End With
Debug.Print Timer - t
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
Application.EnableEvents = True
Application.EnableCancelKey = True
End Sub
the debug print is about 0.3 to 0.5 sec... I have afterwards further wrapped the timer around each of the italic, colorIndex, and value lines and they all turns out take about 0.015 sec each... I have tried searching online how to make the code more efficient, hence the screenupdating toggles as well as no selection, but 0.5 sec still seem a bit slow in updating a cell to me.
please note that I am not whining, I just want to know if I am doing the right thing here. Is there a more efficient way to implement the formatting and value changes that I posted here, or is it just a fact that excel takes this amount of time to update the cell? I am just very curious because the json reading and parsing that I also implemented are significantly faster than this.
Also I have tested this script on at least 3 computers and they all take around the same time so I don't think it's an individual computer problem. And I used excel 2007 and 2010 to test.
I assume you are wanting to format more than a single cell? If so, it will be faster to create a range reference to all the cells requiring the same format (it need not be contiguous), then apply the required format to that range object in one step
Following example demo's creating a range reference, and applying format in one go
Option Explicit
Private Declare Function GetTickCount Lib "kernel32" () As Long
Sub Demo()
Dim t As Long
Dim n As Long, i As Long
Dim m As Long
Dim ws As Worksheet
Dim cl As Range
Dim rSearch As Range
Dim rResult As Range
Set ws = ActiveSheet ' or another sheet...
Set rSearch = ws.Range("A1:A1000")
' note, this is an inefficient loop, can be made much faster
' details will depend on the use case
For Each cl In rSearch
' determine if cell is to be formatted
If cl.Row Mod 2 = 0 Then
' add cl to Result range
If rResult Is Nothing Then
Set rResult = cl
Else
Set rResult = Application.Union(rResult, cl)
End If
End If
Next
Debug.Print "Result Range includes ", rResult.Cells.Count, "Cells"
t = GetTickCount
' Apply format
With rResult
.Font.Italic = True
.Interior.ColorIndex = 37
.Value = 3412
End With
Debug.Print (GetTickCount - t) / 1000, "seconds"
End Sub
In my Excel file I have:
A
1 10-30
2 40-45
3 30-80
There can be any range of numbers separated by - in any cell.
In any particular column (might be any cell) i want to remove all text from the start to the - hyphen.
Example: 40-45 will be replaced with 45.
I've asked this question previously and i got the following solution
Sub Update()
Application.ScreenUpdating = False
Application.EnableEvents = False
Dim ws As Worksheet, ur As Range, r As Range
For Each ws In Sheet
Set ur = ws.UsedRange
For Each r In ur
On Error Resume Next
r = Split(r, "-")(1)
Next
Next
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
But by using the Following script all the worksheets are getting updated.
I want it to updated in selected worksheet and only selected columns like J, K or L.(columns)
Please help me out with this problem.
Sorry if i am missing something here but why don't you just select the column and do Ctrl+h (replace) and in find, write '*-' (without quotes) and click replace all, without writing anything in the replace with field.
I don't understand the need of a macro. Again, apologies if i am missing something here.
This will restrict the replacement to the active sheet, columns J thru L:
Sub Update()
Application.ScreenUpdating = False
Application.EnableEvents = False
Dim ws As Worksheet, ur As Range, r As Range
Set ur = Range("J:L").Cells.SpecialCells(xlCellTypeConstants)
For Each r In ur
If InStr(1, r.Value, "-") > 0 Then
r.Value = Split(r.Value, "-")(1)
End If
Next
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
You'll need to take out the for loop over all the worksheets and pick a range that is only a desired column, not the whole used part of the sheet as it current it. A basic way of doing it:
Sub Update()
Application.ScreenUpdating = False
Application.EnableEvents = False
Dim ws As Worksheet, ur As Range, r As Range
Set ws = ActiveSheet
Set ur = ws.Range(A:A) ' sets the range as a whole column with the chosen letter
For Each r In ur
On Error Resume Next
r = Split(r, "-")(1)
Next
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
You'll just have to replace A:A in the code with whatever column you want e.g. G:G for column G. Depends how exactly you want to picking your columns while using this code.