Speed up excel formatting vba code? - vba

I am using the following vba code to change a text string date into an actual date in excel so I can use it for logical comparisons and the like.
The problem is I need this to work for around 4000 rows and update it weekly, and this code is very slow.
Sub Datechange()
Dim c As Range
For Each c In Range("D2:D" & Range("D" & Rows.Count).End(xlUp).Row)
c.Value = CDate(c.Value)
Next c
End Sub
Are there any alternative ways I could do the same thing quicker? I am assuming part of the reason it is so slow is because there are overheads involved with selecting single cells and processing the code over and over but I am not sure how to do it any other way?
Also some of the rows at the bottom contain the words "None Specified" and when the code reaches these cells it breaks with
Run-time error '13': Type mismatch
Is there a way to stop this happening so the following code can complete?

First steps would be:
Turn screen updating off
Turn calculation off
Read and write the range at once
It could look like the code below - it is a good idea to include an error handler to avoid leaving your spreadsheet with screen updates off or with the calculation mode changed:
Sub Datechange()
On Error GoTo error_handler
Dim initialMode As Long
initialMode = Application.Calculation 'save calculation mode
Application.Calculation = xlCalculationManual 'turn calculation to manual
Application.ScreenUpdating = False 'turn off screen updating
Dim data As Variant
Dim i As Long
'copy range to an array
data = Range("D2:D" & Range("D" & Rows.Count).End(xlUp).Row)
For i = LBound(data, 1) To UBound(data, 1)
'modify the array if the value looks like a date, else skip it
If IsDate(data(i, 1)) Then data(i, 1) = CDate(data(i, 1))
Next i
'copy array back to range
Range("D2:D" & Range("D" & Rows.Count).End(xlUp).Row) = data
exit_door:
Application.ScreenUpdating = True 'turn screen updating on
Application.Calculation = initialMode 'restore original calculation mode
Exit Sub
error_handler:
'if there is an error, let the user know
MsgBox "Error encountered on line " & i + 1 & ": " & Err.Description
Resume exit_door 'don't forget the exit door to restore the calculation mode
End Sub

It would be better to get the values in to an array in one single "pull", operate on the array and write it back.
That would circumvent the expensive range operation.
dim c as range
set c = Range("D2:D" & Range("D" & Rows.Count).End(xlUp).Row)
dim ArrValue() as Variant
set ArrValue = c.value
next step: iterate over that array and then write back:
c.value = Arrvalue
I have no time to test the code, so please correct it for yourself, I am sorry.

Related

Delete Row If Cells Do Not Contain Values

I want to pass all cells in a certain range in column O and to delete all rows that do not contain values: OI and SI.
My code shows me an error at:
If Selection.Value <> "SI" Or "OI" Then
as a type mismatch
Sub CHECK()
Dim MFG_wb As Workbook
Dim Dep As Integer
Dim I As Integer
Set MFG_wb = Workbooks.Open _
("C:\Users\rosipov\Desktop\eliran\MFG - GSS\MFG Daily\Fast Daily " & Format(Now(), "ddmmyy") & ".xlsx", _
UpdateLinks:=False, IgnoreReadOnlyRecommended:=True)
MFG_wb.Sheets("Aleris").Activate
Dep = MFG_wb.Sheets("Aleris").Range("O2", Range("O2").End(xlDown)).Count
Range("O2").Select
For I = 1 To Dep
If Selection.Value <> "SI" Or "OI" Then
EntireRow.Delete
Else
Selection.Offset(1, 0).Select
End If
Next I
End Sub
Try this code to solve your problem. It not only fixes the problematic line, but it avoids some other pitfalls as well that will inevitably cause issues in the long run.
Sub CHECK()
Dim ManufacturingFile As Workbook
Set ManufacturingFile = Workbooks.Open _
("C:\Users\rosipov\Desktop\eliran\MFG - GSS\MFG Daily\Fast Daily " & Format(Now(), "ddmmyy") & ".xlsx", _
UpdateLinks:=False, IgnoreReadOnlyRecommended:=True)
Dim Aleris As Worksheet
Set Aleris = ManufacturingFile.Worksheets("Aleris")
Dim TotalRows As Long
TotalRows = Aleris.Range("O2", Aleris.Range("O2").End(xlDown)).Count
' Avoid Select at all costs
' Range("O2").Select
Dim i As Long
For i = TotalRows To 1 Step -1
If Aleris.Range("O" & i).Value <> "SI" And Aleris.Range("O" & i).Value <> "OI" Then
Aleris.Rows(i).Delete
End If
Next i
End Sub
First, your issue was caused by If Selection.Value <> "SI" Or "OI" Then because "OI" cannot be evaluated as a Boolean statement. Behind the scenes, the interpreter tried to convert "OI" to True or False but was unable to. As a result, you get an error. The fix is simple:
If Selection.Value <> "SI" or Selection.Value <> "OI" Then. Now we have two Boolean statements, both checking for equality. The interpreter is happy with this and can run just fine.
Beyond this, I fixed your unqualified range references, and your practice of Activate and Select. Despite some of the suggestions from others, both of these are very bad habits. Your code will break, and it will cost you. Don't believe me? Read pretty much any other post about Activate and Select and you'll see the same thing.
Why is this a bad idea? You have absolutely no control over what the ActiveSheet is during run-time. Sure you can Activate it, but there will be that time where something comes in and changes the focus to another sheet, and then you'll have issues. This one bug can literally cost hours of work if you're not careful.
The fix is simple. Just declare a variable (as you almost had), and use that variable. Voila! No more worrying about having the wrong sheet.
Finally, Excel is really good at understanding what you mean when you use indices to reference parts of the sheet. You don't have to Selection.Offset(1, 0).Select and then Selection.EntireRow.Delete since all this really means is ActiveSheet.Rows(Selection.Row + 1).Delete and we can refactor that further to use a worksheet, and an index to Foo.Rows(i + 1).Delete. See the pattern here? Become more abstract, step by step, until your code becomes solid.
The last thing I changed was your variable names. Use descriptive names, it makes your code easier to maintain. Also, never ever use underscores "_" in names until you understand Interfaces. Underscores have special meaning to the interpreter.
Finally, check out the Rubberduck project : rubberduckvba.com. It is a free add-in that is dedicated to improving the VBA coding experience. The best part? Most of this feedback is built into RD as inspections. It does the work for you, and you get to learn in the process.
Best of luck!
As Luuklag mentioned, start at the bottom. Also best get the xlLastCell (does not stop at blank cell) to count the rows and adjust the if statement to check for both SI and OI:
Dep = MFG_wb.Sheets("Aleris").Range("O2").SpecialCells(xlLastCell).Row
For I = Dep To 2 Step -1
Cells(I, 15).Select
If Not (Selection.Value = "SI" Or Selection.Value = "OI") Then
Rows(I).Delete
End If
Next I
Individual deleting row is slow.(This delete row many times, so it takes a long time to delete)
After merge range, delete merged range at once.(use Union method)
Sub CHECK()
Dim MFG_wb As Workbook
Dim Dep As Long
Dim i As Long '<~~ if your data is large then use long
Dim Ws As Worksheet
Dim s As String
Dim rngU As Range
Set MFG_wb = Workbooks.Open _
("C:\Users\rosipov\Desktop\eliran\MFG - GSS\MFG Daily\Fast Daily " & Format(Now(), "ddmmyy") & ".xlsx", _
UpdateLinks:=False, IgnoreReadOnlyRecommended:=True)
'MFG_wb.Sheets("Aleris").Activate
Set Ws = MFG_wb.Sheets("Aleris") '<~~ instead activate, use variable
With Ws
Dep = .Range("O2").End(xlDown).Row
'Range("O2").Select '<~~ select mothod is not goo.
For i = 2 To Dep
s = .Range("o" & i)
If s = "SI" Or s = "OI" Then
Else
If rngU Is Nothing Then
Set rngU = .Range("o" & i)
Else
Set rngU = Union(rngU, .Range("o" & i))
End If
End If
Next i
End With
If rngU Is Nothing Then
Else
rngU.EntireRow.Delete
End If
MFG_wb.Save
MFG_wb.Close (0)
End Sub
Just fix line
If Selection.Value <> "SI" Or "OI" Then
To
If Selection.Value <> "SI" Or Selection.Value<>"OI" Then
Once you activated sheet with MFG_wb.Sheets("Aleris").Activate you don't need to explicitly use it with Range objects. After mentioned line, the code should look like:
Dim s As Sheet
Set s = MFG_wb.Sheets("Aleris")
'determine last row in O column
Dep = s.Cells(s.Rows.Count, 15).End(xlUp).Row
For I = 1 To Dep Step -1
If InStr(1, s.Cells(I, 15).Value, "SI") + InStr(1, s.Cells(I, 15).Value, "OI") = 0 Then
s.Cells(I, 15).EntireRow.Delete
End If
Next I
Main reason for the change in a code you posted is that you are using Select method, which isn't a good practice. If you'd be interested, I advise you read why you should avoid using such funtions.

Replacing hard value cells with subtotal formula - VBA

Essentially, our system runs off an expenditure listing of cost headings, with a subtotal on each. The issue being we adjust the data, so need to go through and manually turn the hard value subtotals into subtotal formula in each heading; which over hundreds of different headings, with variable numbers of costs, can be tedious and time consuming.
I've built a basic test example whereby for every instance of A (Heading), where the associated B has a value (an element of data from the system for a line of expenditure), the costs (C) will be subtotalled (109,...), replacing the hard copied value.
Sub insertsubtotal()
Dim cell As Range
Dim sumrange As Range
Set cell = Cells(Cells.Rows.Count, "A")
Do
Set cell = cell.End(xlUp)
Set sumrange = cell.Offset(1, 1).CurrentRegion.Offset(1, 2).Resize(cell.Offset(1, 1).CurrentRegion.Rows.Count - 1, columnsize:=1)
If sumrange.Cells.Count > 1 Then
sumrange.End(xlDown).Offset(2, 0).Formula = "=SUBTOTAL(109," & sumrange.Address & ")"
Else
sumrange.Offset(2, 0).Formula = "=SUBTOTAL(109," & sumrange.Address & ")"
End If
Loop Until cell.Row = 1
End Sub
This works whereby the first heading is in A1, and the cost data in column C as below...
However, where I'm struggling is, I need to amend the process to have the first 5 rows ignored (first heading being on 6), and the cost data and subtotal that needs replacing being in column M.
Any help would be appreciated.
Using SpecialCells to divide the UsedRange in Columns("C") into blocks of contant values, will allow you to easily identify and subtotal your data blocks.
Sub insertsubtotal()
Dim Source As Range, rArea As Range
With Worksheets("Sheet1")
On Error Resume Next
Set Source = Intersect(.UsedRange, .Columns("C")).SpecialCells(xlCellTypeConstants)
On Error GoTo 0
If Source Is Nothing Then
MsgBox "No data found", vbInformation, "Action Cancelled"
Exit Sub
End If
For Each rArea In Source.Areas
rArea.Offset(rArea.Rows.Count).Cells(2).Formula = "=SUBTOTAL(109," & rArea.Address & ")"
Next
End With
End Sub

How to avoid need to activate worksheet every loop

I've set up some VBA code in Excel that asks the user to select a second worksheet, then searches it for a value (a shared key linking the two sets of data, found 6 columns after Rng, where I want to add the retrieved value) in the second table and adds a value from that row to a column in the original table. The part of the program that I would like to adjust is the loop below.
It works fine if when I leave in the line to activate the CurFile workbook. But it means my screen is flashing a lot back and forth between the two workbooks. And once I start getting into hundreds or thousands of lines of data it will be ridiculously slow.
When I comment out that line, the value for FindCID doesn't change and it seems to just keep on refilling the same line, even though the value for r is updating. If after a few loops I add the activate line back in, it resumes properly filling in the results several lines down.
How can I streamline this? I originally was using ThisWorkbook references but even with explicitly defining CurFile (CurFile = ActiveWorkbook.Name) earlier it doesn't seem to go back to that workbook to look up the next value to search for, unless I reactivate the sheet.
Do While r <= maxRows
With Workbooks(CurFile).Worksheets("Sheet1")
Set Rng = .Range(Cells(r, c), Cells(r, c))
End With
FindCID = Rng.Offset(0, 6).Value
If Trim(FindCID) <> "" Then
With Workbooks(FN) ' found earlier by a function
.Activate
End With
With Sheets("Sheet1").Range("D:D")
Set FoundCell = .Find(What:=FindCID)
If Not FoundCell Is Nothing Then
PathLen = FoundCell.Offset(0, 2).Value
Workbooks(CurFile).Sheets("Sheet1").Activate 'If I comment out this line it doesn't work
Rng.Value = PathLen
MsgBox "CID found in " & FoundCell.Address & " Its value is " & PathLen
Else
MsgBox "Nothing found"
End If
End With
End If
On Error Resume Next
r = r + 1
Loop
Actually when working with objects, in most of the cases, there is no need to activate the workbooks\worksheets.
This is your code with some modifications in this regard:
Application.ScreenUpdating = False '(as suggested by CBRF23)
'......
'begining of your code
'......
Do While r <= maxRows
With Workbooks(CurFile).Worksheets("Sheet1")
Set Rng = .Cells(r, c) '(1)
End With
FindCID = Rng.Offset(0, 6).Value2
If Trim(FindCID) <> "" Then
Set FoundCell = Workbooks(FN).Sheets("Sheet1").Range("D:D").Find(What:=FindCID)
If Not FoundCell Is Nothing Then Rng.Value = FoundCell.Offset(0, 2).Value2
End If
r = r + 1
Loop
'......
'rest of your code
'......
Application.ScreenUpdating = True
(1) Notice that way the Range is defined as it’s made of only once Cell; but if the range has more than one Cell i.e. from Cell(r,c) to Cell(r,c+5) then you need to use the form:
Set Rng = Range(.Cells(r, c), .Cells(r, c+5))
There is no need to add a period . before Range as the range is defined by the Cells within the Range command. By using the period . before the Cell command they are referred as part of the
With Workbooks(CurFile).Worksheets("Sheet1")
However if the Range is defined as A1:F1 then the period . has to be added before the Range as in:
Set Rng = .Range(“A1:F1”)
I removed the MsgBox commands as I believe they were just for testing purposes. Not really showing these messages for hundreds or thousands lines of data. Isn’t it?

Automatic spreadsheet generation in Excel VBA

My friend and I currently have a master spreadsheet that I need to be broken out into smaller spreadsheets regularly. This used to be a manual process, but I'd like to automate it. I created a three step solution in VBA which would help me accomplish this that did the following:
Apply relevant filters to spreadsheet
Export data currently visible after filter into new spreadsheet
Save spreadsheet and go back to 1 (different criteria)
Unfortunately I am having a hard time implementing it. Whenever I try to generate the spreadsheet, my document hangs, starts performs several calculations and then gives this me this error message:
Upon debugging the code, I get an error message at this line:
One Excel workbook is left open and only one row is visible (the second row pulled from the Master which contains header information) and nothing else.
What exactly is going on here?
This is my code so far:
The heart of it all
' This bit of code get's all the primary contacts in column F, it does
' this by identifying all the unique values in column F (from F3 onwards)
Sub GetPrimaryContacts()
Dim Col As New Collection
Dim itm
Dim i As Long
Dim CellVell As Variant
'Get last row value
LastRow = Cells.SpecialCells(xlCellTypeLastCell).Row
'Loop between all column F to get unique values
For i = 3 To LastRow
CellVal = Sheets("Master").Range("F" & i).Value
On Error Resume Next
Col.Add CellVal, Chr(34) & CellVal & Chr(34)
On Error GoTo 0
Next i
' Once we have the unique values, apply the TOKEN NOT ACTIVATED FILTER
Call TokenNotActivated
For Each itm In Col
ActiveSheet.Range("A2:Z2").Select
Selection.AutoFilter Field:=6, Criteria1:=itm
' This is where the magic happens... creating the individual workbooks
Call TokenNotActivatedProcess
Next
ActiveSheet.AutoFilter.ShowAllData
End Sub
The "token not activated" filter
Sub TokenNotActivated()
'Col M = Yes
'Col U = provisioned
ThisWorkbook.Sheets(2).Activate
ActiveSheet.Range("A2:Z2").Select
Selection.AutoFilter Field:=13, Criteria1:="Yes"
Selection.AutoFilter Field:=21, Criteria1:="provisioned", Operator:=xlFilterValues
End Sub
Running the process to get the workbooks saved
Function TokenNotActivatedProcess()
Dim r As Range, n As Long, itm, FirstRow As Long
n = Cells(Rows.Count, 1).End(xlUp).Row
Set r = Range("A1:A" & n).Cells.SpecialCells(xlCellTypeVisible)
FirstRow = ActiveSheet.Range("F2").End(xlDown).Row
itm = ActiveSheet.Range("F" & FirstRow).Value
If r.Count - 2 > 0 Then Debug.Print itm & " - " & r.Count - 2
Selection.SpecialCells(xlCellTypeVisible).Select
Selection.Copy
Workbooks.Add
ActiveSheet.Paste
Application.CutCopyMode = False
ActiveWorkbook.SaveAs Filename:="C:\Working\Testing\TokenNotActivated - " & itm + ".xls", FileFormat:=52, CreateBackup:=False
End Function
This error is caused by trying to filter an empty range. After analysing your code, my guess is that you are missing a worksheet activation here, since repeating the line ActiveSheet.Range("A2:Z2").Select after calling the function TokenNotActivated does not make sense and maybe your code is trying to filter some empty range/worksheet.

Excel VBA: Compiler Errors

So yesterday I posted my first SO question, and it went down like a ton of bricks. However I've picked myself up, dusted myself off, and hopefully this question will be more acceptable... :-)
I am trying to remove data duplicates from a list of Health Questionnaires I have to monitor, but the tricky bit I was struggling with was finding a duplicate in one column, AND then checking that the data on the same row, for the 3 adjacent columns were also duplicates. Storing the searched for 'duplicated row' was the bit that was throwing me off.
Here's some code I've cobbled together from other similarly-functioning scripts. I'm now in debug mode and keep getting errors thrown up... I don't have much experience of VBA, so i'm running out of options.
I'm currently getting type mismatch errors with the variable g, and also firstAddress. Why are these causing problems???
Can I call firstAddress.Row or am I barking up the wrong tree?
Here's the snippet:
g = .Find(Range("G" & i).Text, LookIn:=xlValues)
If Not g Is Nothing Then
firstAddress = g.Address
dupRow = firstAddress.Row
And here's the whole code below. Any help would be much appreciated!
Sub FindCpy()
Dim lw As Long
Dim i As Integer
Dim sh As Worksheet
Dim dupRow As Integer
Dim g As Integer
Dim firstAddress As Integer
'Used for the new worksheet we are pasting into
Dim objNewSheet As Worksheet
Dim rngNextAvailbleRow As Range
'Used to narrow down the logical operators for duplicates
Dim rngFirst As Range
'Set the ranges
rngFirst = Range("G" & 1, "G" & lw)
Set sh = Sheets("Completed")
lw = Range("A" & Rows.Count).End(xlUp).Row
For i = 1 To lw 'Find duplicates from the list.
If Application.CountIf(Range("A" & i & ":A" & lw), Range("A" & i).Text) = "Complete" Then
'if COMPLETE, check the rest of the sheet for any 'in progress' duplicates...
With Worksheets("Still In Progress").rngFirst
g = .Find(Range("G" & i).Text, LookIn:=xlValues)
If Not g Is Nothing Then
firstAddress = g.Address
dupRow = firstAddress.Row
If Range("H" & dupRow).Text = Range("H" & i).Text _
And Range("I" & dupRow).Text = Range("I" & i).Text _
And Range("J" & dupRow).Text = Range("J" & i).Text Then
'select the entire row
Range.EntireRow.Select
'copy the selection
Selection.Cut
'Now identify and select the new sheet to paste into
Set objNewSheet = ThisWorkbook.Worksheets("Completed")
objNewSheet.Select
'Looking at your initial question, I believe you are trying to find the next available row
Set rngNextAvailbleRow = objNewSheet.Range("A1:A" & objNewSheet.Cells(Rows.Count, "A").End(xlUp).Row)
Range("A" & rngNextAvailbleRow.Rows.Count + 1).Select
ActiveSheet.Paste
'delete the initial row
rngCell.EntireRow.Delete
Set g = .FindNext(g)
Loop While Not g Is Nothing And g.Address <> firstAddress
End If
End With
Next i
End Sub
I went through your code carefully. There were a number of problems. Some of these I think I was able to fix - there was one where I guessed what you intended to do, but for one of them I just marked it; you need to explain what you were trying to do, as you are deleting a range that you never defined...
The first problem is with the line:
If Application.CountIf(Range("A" & i & ":A" & lw), Range("A" & i).Text) = "Complete" Then
The CountIf function returns a number; you are comparing this number with the string "Complete". I don't think you can ever get past this line, so the rest of the code (whether correct or not) will not execute. Not entirely clear what you are trying to do in this line, as I'm not sure when a line will be marked "Complete" - but assuming that you are interested in executing the rest of the code if the cell in A & i has the string "Complete" in it, then you probably want to do
If Range("A" & i).Text = "Complete" Then
There were a number of If - Then, With, and Loop structures that were not properly terminated with a matching End. I have tried to remedy this - make sure I did it right. Note that using proper indentation really helps to find problems like this. The space bar is your friend...
Since the Find method returns an object, the correct way to use the function is
Set g = .Find(Range("G" & i).Text, LookIn:=xlValues)
Apart from that - use Option Explicit at the top of your code, and define variables with the most restrictive (correct) type that you can. When I did this I found the error I could not correct - with the rngCell variable that was neither declared, nor ever set... It shows just how helpful it can be. Also good for catching typos - VBA will happily let you write things like
myVar = 1
MsgBox myVra + 1
The message will be 1, not 2, because of the typo... The fact that Explicit should even be an option is one of the many inexplicable design decisions made by the VBA team.
Here is your code "with most of the errors fixed". At least like this it will compile - but you must figure out what to do with the remaining error (and I can't be sure I guessed right about what you wanted to do with the cell marked "Complete").
Comments welcome.
Option Explicit
Sub FindCpy()
Dim lw As Long
Dim i As Integer
Dim sh As Worksheet
Dim dupRow As Integer
Dim g As Range
Dim firstAddress As Range
'Used for the new worksheet we are pasting into
Dim objNewSheet As Worksheet
Dim rngNextAvailbleRow As Range
'Used to narrow down the logical operators for duplicates
Dim rngFirst As Range
'Set the ranges
rngFirst = Range("G" & 1, "G" & lw)
Set sh = Sheets("Completed")
lw = Range("A" & Rows.Count).End(xlUp).Row
For i = 1 To lw 'Find duplicates from the list.
' If Application.CountIf(Range("A" & i & ":A" & lw), Range("A" & i).Text) = "Complete" Then
If Range("A" & i).Text = "Complete" Then
'if COMPLETE, check the rest of the sheet for any 'in progress' duplicates...
With Worksheets("Still In Progress").rngFirst
Set g = .Find(Range("G" & i).Text, LookIn:=xlValues)
If Not g Is Nothing Then
firstAddress = g.Address
dupRow = firstAddress.Row
If Range("H" & dupRow).Text = Range("H" & i).Text _
And Range("I" & dupRow).Text = Range("I" & i).Text _
And Range("J" & dupRow).Text = Range("J" & i).Text Then
'select the entire row
g.EntireRow.Select
'copy the selection
Selection.Cut
'Now identify and select the new sheet to paste into
Set objNewSheet = ThisWorkbook.Worksheets("Completed")
objNewSheet.Select
'Looking at your initial question, I believe you are trying to find the next available row
Set rngNextAvailbleRow = objNewSheet.Range("A1:A" & objNewSheet.Cells(Rows.Count, "A").End(xlUp).Row)
Range("A" & rngNextAvailbleRow.Rows.Count + 1).Select
ActiveSheet.Paste
'delete the initial row
rngCell.EntireRow.Delete ' <<<<<< the variable rngCell was never defined. Cannot guess what you wanted to do here!
Do
Set g = .FindNext(g)
Loop While Not g Is Nothing And g.Address <> firstAddress
End If ' entire row matched
End If ' Not g Is Nothing
End With ' With Worksheets("Still in Progress")
End If ' CountIf = "Complete"
Next i
End Sub
Another handy trick: when you "paste in the next available row" as you are doing with Range("A" & rngNextAvailbleRow.Rows.Count + 1).Select, I usually find it handy to do something like this instead:
Dim destination As Range
Set destination = Worksheets("Sheetname").Range("A1")
And when you need to paste something:
destination.Select
ActiveSheet.Paste
Set destination = destination.Offset(1,0)
This way, destination is always pointing to the "next place where I can paste". I find it helpful and cleaner.