How can i prevent duplicates from being created - excel

I've created an excel spreadsheet which will be tracking faulty assets that have been sent back from our external agents, I have two tabs that will be the main focus of this spreadsheet. Tab 1 will be the tested assets tab and Tab 2 will be awaiting testing. So once any asset that gets sent back will be manually logged on the awaiting testing tab, but once it's been tested I've created a vba code that will export anything that's been marked with a "Y" meaning it's been tested on to the Tested assets tab.
But the problem i have here is that one asset can come in to testing and be sent back out to the field to our engineers more than once, so if it comes back to be tested again and gets logged on the awaiting testing and once it's been tested and exported to the tested assets tab it duplicates what already is on the tested asset tab and i get two cells with the same data. Is there anyway i can put in another line of code that will prompt me on a duplicate before it exports it. See code below;
Sub automove()
Dim SerialNo As String
Dim AwaitTestLastRow, PasteToRow As Long
Sheets("Awaiting Testing").Select
AwaitTestLastRow = Range("a1000000").End(xlUp).Row
For x = AwaitTestLastRow To 3 Step -1
If Range("c" & x).Value = "Y" Or Range("c" & x).Value = "y" Then
SerialNo = Range("a" & x).Value
Rows(x).Delete
Sheets("Tested Assets").Select
Range("a1000000").End(xlUp).Offset(1, 0).Value = SerialNo
Range("e1000000").End(xlUp).Offset(1, 0).Value = SerialNo
PasteToRow = Range("a1000000").End(xlUp).Row
Range("b3:d3").Select
Selection.Copy
Range("b" & PasteToRow & ":d" & PasteToRow).Select
ActiveSheet.Paste
Range("f3").Select
Selection.Copy
Range("f" & PasteToRow & ":f" & PasteToRow).Select
ActiveSheet.Paste
Sheets("Awaiting Testing").Select
End If
Next x

There are many different ways of checking for duplicates. In the code below, I've used a .Find function on the "Tested Assets" worksheet. If the return object is Nothing then it's a new item, if it's a Range then we know the address of your duplicate. It's not necessarily the quickest way (a Collection might be quicker, for example), but the .Find function is still pretty snappy and as, you'll see in my next comment, I wanted to have the range address.
I've put some code below that instead of prompting you for a duplicate, records the frequency with which the same item is returning to the test lab - might be of some use to you for tracking a repeat offender. However, if you don't want that then delete the 4 lines and replace with MsgBox asset(1, 1) & " is a duplicate."
I've adjusted your code slightly to speed it up, and watch out for declaring two variables on the same line as each variable must have its own declaration type. In your line: Dim AwaitTestLastRow, PasteToRow As Long, the AwaitTestLastRow variable isn't a Long (it's actually not typed ie a Variant).
Sub AutoMove_v2()
Dim awaitingRange As Range
Dim testedRange As Range
Dim flaggedRange As Range
Dim newRow As Range
Dim dupCell As Range
Dim testFlag As String
Dim asset As Variant
Dim cell As Range
Dim frq As Long
'Initialise the parameters
With ThisWorkbook.Worksheets("Awaiting Testing")
Set awaitingRange = .Range("A3", _
.Cells(.Rows.Count, "A").End(xlUp))
End With
With ThisWorkbook.Worksheets("Tested Assets")
Set testedRange = .Range("A1", _
.Cells(.Rows.Count, "A").End(xlUp))
End With
'Loop through the awaiting sheet to find assets for transferral
For Each cell In awaitingRange
testFlag = UCase(cell.Offset(, 2).value)
If testFlag = "Y" Then
If flaggedRange Is Nothing Then
Set flaggedRange = cell
Else
Set flaggedRange = Union(flaggedRange, cell)
End If
End If
Next
'Identify duplicates or transfer new assets
For Each cell In flaggedRange
asset = cell.Resize(, 4).value
Set dupCell = testedRange.Cells.Find(What:=asset(1, 1), _
After:=testedRange.Cells(1), _
LookIn:=xlFormulas, _
LookAt:=xlWhole, _
SearchOrder:=xlByRows, _
SearchDirection:=xlNext, _
MatchCase:=True)
If dupCell Is Nothing Then
'It's a new entry so transfer the values
Set newRow = testedRange.Cells(testedRange.Cells.Count).Offset(1)
Set testedRange = Union(testedRange, newRow)
newRow.Resize(, 4) = asset
Else
'It's a duplicate so increment the frequency counter
frq = dupCell.Offset(, 5).value
If frq = 0 Then frq = 1
frq = frq + 1
dupCell.Offset(, 5) = frq
End If
Next
'Delete the transferred rows
flaggedRange.EntireRow.Delete
End Sub

Related

Paste into next empty row of another worksheet

The below code, when a user logs an issue into a form, will log in the appropriate issue tab.
Once the team has completed the issue and marks it as "Complete & Verified", I want to move that issue (row) out of the current tab into the "5. Complete & Verified" tab.
The issue is, say there are 9 rows of data in the current tab, the macro is pasting the row into the 9th row of the "5. Complete & Verified" tab.
I am trying to paste one after the other starting in B2. I am also trying to Paste the tab name into column 1 (column A) as an identifier.
Sub Complete()
ActiveSheet.Activate
Dim objWS As Worksheet
Set objWS = ActiveSheet
Dim intLastRowSrc As Long
intLastRowSrc = ActiveSheet.Cells.Find("*", searchorder:=xlByRows, searchdirection:=xlPrevious).Row
ActiveSheet.Activate
Dim intLastRowSDes As Long
intLastRowSDes = ActiveSheet.Cells.Find("*", searchorder:=xlByRows, searchdirection:=xlPrevious).Row + 1
Dim r As Long
For r = 2 To intLastRowSrc
If objWS.Cells(r, "R") = "Complete & Verified" Then
Sheets("5. Complete & Verified").Range("B" & intLastRowSDes & ":T" & intLastRowSDes).Value = objWS.Range("A" & r & ":S" & r).Value
objWS.Rows(r).Delete
Sheets("5. Complete & Verified").Cells(intLastRowSDes, 1) = ws1.Name
intLastRowSrc = intLastRowSrc - 1
intLastRowSDes = intLastRowSDes + 1 'Issue - I need it to paste into next row with now data in 5. tab
End If
Next
Exit Sub
There are a few things that need adjusted to work as (I think) you want things to work.
Firstly, set references to the source and destination worksheets and use them directly rather than naming each time.
Secondly you don't need to Activate any of the worksheets, so let's remove those
Thirdly if you are looking to delete rows within a for loop, always start at the bottom of your data range and move up- otherwise when you delete row 21 and 22 moves up, your loop will completely ignore the fact that 22 moved up without getting checked and you will miss rows
Fourthly, just grab the destination row from inside the loop rather than try to increment the count
Fifthly, you are setting column 1 on your destination sheet to ws1.name but you never define it, so I've replaced that with a reference to the source worksheet name.
If any of this doesn't make sense, drop a comment below and I'll try to explain better.
Sub Complete()
Dim sourceWS As Worksheet
Set sourceWS = ActiveSheet
Dim destWS As Worksheet
Set destWS = ThisWorkbook.Worksheets("5. Complete & Verified")
Dim intLastRowSrc As Long
intLastRowSrc = sourceWS.Cells.Find("*", searchorder:=xlByRows, searchdirection:=xlPrevious).Row
Dim intLastRowSDes As Long
Dim r As Long
For r = intLastRowSrc to 2 Step -1
If sourceWS.Cells(r, "R") = "Complete & Verified" Then
intLastRowSDes = destWS.Cells.Find("*", searchorder:=xlByRows, searchdirection:=xlPrevious).Row + 1
destWS.Range("B" & intLastRowSDes & ":T" & intLastRowSDes).Value = sourceWS.Range("A" & r & ":S" & r).Value
sourceWS.Rows(r).Delete
destWS.Cells(intLastRowSDes, 1) = sourceWS.Name
End If
Next
Exit Sub
Michael,
Dave posted his answer while I was working on it. While it will work, if I'm not mistaken, and that may well be the case, the items will be copied in reverse order to the new destination sheet. If order is important you may what to try using a Do/Loop as follows:
Option Explicit
Sub Complete()
Dim lRow As Long
Dim shtWS As Worksheet
Dim shtDest As Worksheet
Dim lLastRowSDes As Long
'*** Don't use ActiveSheet rather specify the name
'*** If called from more than one sheet pass as parameter.
Set shtWS = WorkSheets("your sheet name here")
set shtDst = Worksheets("5.Complete & Verified")
lLastRowSDes = ActiveSheet.Cells.Find("*", _
searchorder:=xlByRows, _
searchdirection:=xlPrevious).Row + 1
lRow = 2 'Set Starting Row
Do
If shtWS.Cells(lRow, "R") = "Complete & Verified" Then
shtWS.Range("B" & lRow & ":T" & lRow).Copy
shtDst.Range("B" & lLastRowSDes).Paste
shtWS.Rows(lRow).Delete
'*** Note we don't increment counter as next row moves up to current lRow position!
shtDst.Cells(lLastRowSDes, 1) = shtWS.Name
lLastRowSDes = lLastRowSDes + 1
Else
lRow = lRow + 1 'Increment Row Counter
End If
Loop Until (shtWS.Cells(lRow,"B").Value = "")
You'll notice I used Copy/Paste as I've never seen the syntax of assigning one range to another like that, very neat! So you could just replace the copy/paste lines with that one.
FYI: code not tested!
HTH

Copy row of data based on criteria AND "label" that copied data in last column

I have working code that checks for a criteria in each row, and if met, copies that whole row of data over to a different workbook. But! I need to be able to add text to the last column of the copied data (Column S) that essentially labels what criteria was met that made the code copy it over because I will soon be expanding to check for multiple different criteria.
So for every row that meets the criteria and gets copied, I want to add "Criteria1" next to it in column S in the new workbook (it will always be column S that will be the first available column).
I have mangled this code together through inheritance and all of your help, so I don't really even know where to begin.
Private Sub Workbook_AfterSave(ByVal Success As Boolean)
Dim CoderBook As Workbook
Dim Referrals As Worksheet
Dim Review As Workbook
Dim Crit As Worksheet
Dim LastRow As Long
Dim NextRow As Long
Dim i As Long
Set CoderBook = Workbooks.Open("Coder Referrals.xlsx")
Set Referrals = CoderBook.Sheets("Sheet1")
Set Review = ThisWorkbook
Set Crit = Review.Sheets("Criteria")
'Search code
LastRow = Crit.Cells.Find(What:="*", After:=[A1], SearchOrder:=xlByRows, SearchDirection:=xlPrevious).Row
Application.ScreenUpdating = False
'Loop search code
For i = 2 To LastRow
'Specialized Criteria1 Check
If Crit.Range("F" & i) <> Crit.Range("G" & i) Or _
Crit.Range("I" & i) <> Crit.Range("J" & i) Then
'If meets Criteria1 check, then copy appropriate rows to CoderBook Referrals sheet
Referrals.Cells(Rows.Count, 1).End(xlUp).Offset(1, 0).EntireRow.Value = Crit.Rows(i).Value
End If
Next i
'End loop code
CoderBook.Close SaveChanges:=True
Application.ScreenUpdating = True
End Sub
Split the or into two statements:
For i = 2 To LastRow
j = Referrals.Cells(Rows.Count, 1).End(xlUp).row + 1
'Specialized Criteria1 Check
If Crit.Range("F" & i) <> Crit.Range("G" & i) Then
'If meets Criteria1 check, then copy appropriate rows to CoderBook Referrals sheet
Referrals.Rows(j).EntireRow.Value = Crit.Rows(i).Value
Referrals.Range("S" & j).Value = "Criteria1"
End If
If Crit.Range("I" & i) <> Crit.Range("J" & i) Then
Referrals.Rows(j).EntireRow.Value = Crit.Rows(i).Value
if Referrals.Range("S" & j).value = vbNullString then
Referrals.Range("S" & j).Value = "Criteria2"
Else
Referrals.Range("S" & j).Value = Referrals.Range("S" & j).Value & ", " & "Criteria2"
End if
Next i

Fastest way to transfer large amounts of data between worksheets

I currently have 2 worksheets, for simplicity sake let's call them Sheet1 and Sheet2 in the explanations. In Sheet1 I have around 50k rows of data. I am trying to go through Sheet1 and find unique occurrences in the data set to then transfer across to Sheet2.
Below are the methods I have used so far and their rough estimates for time taken.
Method A - Iterate through Sheet1 with a For loop with the conditional check programmed in VBA, if condition is met - transfer a range of 8 cells on that row to Sheet2. This method completes 60% in 60 minutes.
Method B - I thought that removing the condition check in VBA could speed things up so I created a new column in Sheet1 with an IF statement that returns "Y" if the condition is met. I then iterate through this column and if there is a "Y" - transfer the occurrence across to Sheet2. This weirdly takes longer than method A, namely 50% in 60 mins.
Sub NewTTS()
Dim lRow1 As Long, lRow2 As Long
Dim i As Long
With wsOTS
lRow1 = .Range("E" & .Rows.Count).End(xlUp).Row
For i = lRow1 To 2 Step -1
If .Range("P" & i).Text = "Y" Then
lRow2 = wsTTS.Range("E" & wsTTS.Rows.Count).End(xlUp).Row + 1
wsTTS.Range("E" & lRow2, "AA" & lRow2).Value = .Range("E" & i, "AA" & i).Value
End If
Next i
End With
End Sub
Method C - I then read on another post that the .Find() method is quicker than using For loop method. As such I used a .Find() in the column that returns the "Y" and then transfer event across to Sheet2. This is the fastest method so far but still only completes 75% in 60 mins.
Sub SearchOTS()
Application.ScreenUpdating = False
Dim startNumber As Long
Dim lRow1 As Long, lRow2 As Long
Dim i As Long
Dim startTime As Double
startTime = Time
lRow1 = wsOTS.Range("E" & wsOTS.Rows.Count).End(xlUp).Row
Columns("P:P").Select
Selection.Find(What:="Y", After:=ActiveCell, LookIn:=xlValues, LookAt:= _
xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False _
, SearchFormat:=False).Activate
startNumber = ActiveCell.Row
lRow2 = wsTTS.Range("E" & wsTTS.Rows.Count).End(xlUp).Row + 1
wsTTS.Range("E" & lRow2, "AA" & lRow2).Value = wsOTS.Range("E" & ActiveCell.Row, "AA" & ActiveCell.Row).Value
For i = 1 To lRow1
Selection.FindNext(After:=ActiveCell).Activate
If ActiveCell.Row = startNumber Then GoTo ProcessComplete
lRow2 = wsTTS.Range("E" & wsTTS.Rows.Count).End(xlUp).Row + 1
wsTTS.Range("E" & lRow2, "AA" & lRow2).Value = wsOTS.Range("E" & ActiveCell.Row, "AA" & ActiveCell.Row).Value
wsOTS.Range("B18").Value = i / lRow1
Next i
ProcessComplete:
Application.ScreenUpdating = True
MsgBox "Complete! Time taken: " & Format(Time - startTime, "hh:mm:ss")
End Sub
Method D - I then read another post saying that the fastest way would be to build an array and then loop through the array. Instead of an array I used a collection (dynamic), and I iterate through Sheet1 and store the row numbers for the occurences. I then loop through the collection and transfer the events across to Sheet2. This method returns 50% in 60 mins.
Sub PleaseWork()
Dim i As Long
Dim lRow1 As Long, lRow2 As Long
Dim myCol As New Collection
lRow1 = wsOTS.Range("E" & wsOTS.Rows.Count).End(xlUp).Row
'build collection of row numbers
For i = 1 To lRow1
If wsOTS.Range("P" & i).Text = "Y" Then
myCol.Add i
End If
Next i
'now go through collection and build TTS
For i = 1 To myCol.Count
lRow2 = wsTTS.Range("E" & wsTTS.Rows.Count).End(xlUp).Row + 1
wsTTS.Range("E" & lRow2, "N" & lRow2).Value = wsOTS.Range("E" & myCol(i), "N" & myCol(i)).Value
Next i
Set myCol = New Collection
End Sub
I am trying to find the fastest way to complete this task but all the methods I have tried are yielding greater than an hour to complete.
Is there anything I am missing here? Is there a faster method?
Accessing a range is abysmally slow, and the cause for your long runtime. If you already know that you are going to read 1000 rows, do not read them one at a time. Instead, pull the whole range in a buffer, then work only with that buffer. Same goes for writing. If you do not know in advance how much you will write, make chunks of e.g. 100 rows length.
(Untested) example:
Sub PleaseWork()
Dim i As Long, j as long
Dim lRow1 As Long, lRow2 As Long
Dim myCol As New Collection
Dim column_p() as variant
dim inbuffer() as Variant
dim outbuffer() as variant
lRow1 = wsOTS.Range("E" & wsOTS.Rows.Count).End(xlUp).Row
' Get whole Column P at once
column_p = wsOTS.Range("P1").Resize(lRow1, 1).Value
'build collection of row numbers
For i = 1 To lRow1
If column_p(i, 1) = "Y" Then
myCol.Add i
End If
Next i
'now go through collection and build TTS
lRow2 = myCol.Count 'Number of required rows
' get whole input range
inbuffer = wsOTS.Range("E1").Resize(lRow1, 10).Value
' prepare output
ReDim outbuffer(1 to lRow2, 1 to 10)
For i = 1 To myCol.Count
' write into outbuffer
for j = 1 to 10
outbuffer(i, j) = inbuffer(myCol(i), j)
Next
Next i
' Set whole output at once
wsTTS.Range("E1").Resize(lRow2, 10).Value = outbuffer
Set myCol = New Collection
End Sub
did you consider using Remove Duplicates.
Steps:
Copy entire data to a new sheet
On Data tab, choose Remove duplicates
You can record this as a macro as well.

Merge empty cells with previous value

I have an Excel file with around 100,000 records. I have 6+ columns, the first five of which are:
Required Format:
So far I have :
Sub Main()
Dim i As Long
Dim j As Long
Dim sameRows As Boolean
sameRows = True
For i = 1 To Range("A" & Rows.Count).End(xlUp).Row
For j = 1 To 4
If StrComp(Cells(i, j), Cells(i + 1, j), vbTextCompare) Then
sameRows = False
End If
Next j
If sameRows Then
Range(Cells(i, 4), Cells(i + 1, 4)).merge
End If
sameRows = True
Next i
End Sub
I am able to get below by running the macro by changing value in Range from 4 to 1/2/3/4 and running macro four times.
Please help me get the data in required format. I still need to merge the empty fields with the previous non empty field.
Pratik, listen carefully to Jeeped. Working with large data in Excel isn't ideal, and working with raw data in merged cells is staring into the abyss - it's a dark, dark place where Range referencing and things like Offset functions will show you a dimension of despair you never knew existed.
If you have this data in another format, say XML, that you've imported into Excel then use VBA to read the data, query it, etc. in its original format. If it exists in a database, then, again, use VBA to access that database and manipulate the recordsets as you wish. If this is your only source of data, then why not write it into an XML document or into VBA's own data storage options (like Collection or arrays).
If you have to use Excel then don't confuse raw data with data display. Yes, the merged cells might be easier to read for the human eye, but I'd just pose the question: is that your primary objective in conducting the merge?
If you must take that leap into the abyss - and you can see that at least two of us would advise against - then at least speed things up by reading from an array and merging rows at a time:
Sub OpenDoorsToHades()
Dim dataSheet As Worksheet
Dim v As Variant
Dim mergeCells As Range
Dim mergeAreas As Range
Dim i As Long
Dim blankStart As Long
Dim blankEnd As Long
Dim doMerge As Boolean
Dim c As Integer
Set dataSheet = ThisWorkbook.Worksheets("data") 'rename to your sheet
'Read values into array of variants
With dataSheet
v = .Range("A1", .Cells(.Rows.Count, "A").End(xlUp)).Value2
End With
'Check for blanks
For i = 1 To UBound(v, 1)
If IsEmpty(v(i, 1)) Then
If Not doMerge Then
blankStart = i - 1
doMerge = True
End If
Else
If doMerge Then
blankEnd = i - 1
For c = 1 To 4
With dataSheet
Set mergeCells = .Range( _
.Cells(blankStart, c), _
.Cells(blankEnd, c))
If mergeAreas Is Nothing Then
Set mergeAreas = mergeCells
Else
Set mergeAreas = .Range(mergeAreas.Address & _
"," & mergeCells.Address)
End If
End With
Next
mergeAreas.Merge
Set mergeAreas = Nothing
doMerge = False
End If
End If
Next
'Format the sheet
dataSheet.Cells.VerticalAlignment = xlCenter
Application.ScreenUpdating = True
End Sub
How about just populating the empty cells with the values above, so the values on the far right are associated with the same values that would've been in the merged cell. For example, if 19 is in cell A2, you can recreate the table starting in G2 with =IF(A2<>"",A2,G1), and this way all empty cells will be populated with the value above, pairing the values at the far right with the exact same values.
I tackled the same problem myself this week. Ambie's solution seemed overly complex, so I wrote something pretty simple to merge rows:
Sub MergeRows()
Sheets("Sheet1").Select
Dim lngStart As Long
Dim lngEnd As Long
Dim myRow As Long
'Disable popup alerts that appear when merging rows like this
Application.DisplayAlerts = False
lngStart = 2
lngEnd = 2
For myRow = 2 To Range("A" & Rows.Count).End(xlUp).Row 'last row
If Range("A" & (myRow + 1)).value = "" Then
'include row below in next merge
lngEnd = myRow + 1
Else
'merge if 2+ rows are included
If lngEnd - lngStart > 0 Then
Range("A" & lngStart & ":A" & lngEnd).Merge
Range("B" & lngStart & ":B" & lngEnd).Merge
Range("C" & lngStart & ":C" & lngEnd).Merge
Range("D" & lngStart & ":D" & lngEnd).Merge
End If
'reset included rows
lngStart = myRow + 1
lngEnd = myRow + 1
End If
Next myRow
Application.DisplayAlerts = True
End Sub

Saving Excel data as csv with VBA - removing blank rows at end of file to save

I am creating a set of csv files in VBA.
My script is creating the data set I need, but the number of rows differs in multiple iterations of the loop. For instance, for i=2, I have 100,000 rows, but for i=3, I have 22,000 rows. The problem is that when Excel saves these separate csv files, it does not truncate the space at the end. This leaves 78,000 blank rows at the end of the file, which is an issue given that I need about 2,000 files to be generated, each several megabytes large. (I have some data I need in SQL, but can't do the math in SQL itself. Long story.)
This problem normally occurs when saving manually - you need to close the file after removing the rows, then reopen, which is not an option in this case, since it's happening automatically in VBA. Removing the blank rows after saving using a script in another language isn't really an option, since I actually need the output files to fit on the drive available, and they are unnecessarily huge now.
I have tried Sheets(1).Range("A2:F1000001").ClearContents, but this does not truncate anything. Removing the rows should have similarly no effect before saving, since Excel saves all rows until the end of the file, as it stores the bottom-right most cell operated on. Is there a way to have excel save only the rows I need?
Here is my code used to save: (The truncation happens earlier, in the routing that calls this one)
Sub SaveCSV()
'Save the file as a CSV...
Dim OutputFile As Variant
Dim FilePath As Variant
OutputPath = ActiveWorkbook.Worksheets("Macro").Range("B2").Value
OutputFile = OutputPath & ActiveWorkbook.Worksheets("Macro").Range("B1").Value
Application.DisplayAlerts = False 'DISABLE ALERT on Save - overwrite, etc.
ActiveWorkbook.SaveAs Filename:=OutputFile, FileFormat:=xlCSV, CreateBackup:=False
Application.DisplayAlerts = True 'DISPLAY ALERTS
End Sub
The relevant bit of code is here:
'While looping through Al, inside of looping through A and B...
'Created output values needed in this case, in an array...
Sheets(1).Range("A2:E90001") = Output
ActiveWorkbook.Worksheets(1).Range("F2").Formula = "=(does not matter, some formula)"
ActiveWorkbook.Worksheets(1).Range("F2").AutoFill Destination:=Range("F2:F90001")
'Set Filename to save into...
ActiveWorkbook.Worksheets("Macro").Range("B1").Value = "Values_AP" & Format(A, "#") & "_BP" & Format(B, "#") & "_Al" & Format(Al, "#")
'Save Sheet and reset...
Call SaveCSV
Sheets(1).Range("A2:F90001").ClearContents
CurrRow = 1
Next Al
You can get the UsedRange to recalculate itself without deleting columns and rows with a simple
ActiveSheet.UsedRange
Alternatively you can automate the manual removal of the "false" usedrange by deleting the areas below the last used cell with code such as DRJ's VBAexpress article, or by using an addin such as ASAP Utilities
The function from DRJ's article is;
Option Explicit
Sub ExcelDiet()
Dim j As Long
Dim k As Long
Dim LastRow As Long
Dim LastCol As Long
Dim ColFormula As Range
Dim RowFormula As Range
Dim ColValue As Range
Dim RowValue As Range
Dim Shp As Shape
Dim ws As Worksheet
Application.ScreenUpdating = False
Application.DisplayAlerts = False
On Error Resume Next
For Each ws In Worksheets
With ws
'Find the last used cell with a formula and value
'Search by Columns and Rows
On Error Resume Next
Set ColFormula = .Cells.Find(What:="*", After:=Range("A1"), LookIn:=xlFormulas, _
LookAt:=xlPart, SearchOrder:=xlByColumns, SearchDirection:=xlPrevious)
Set ColValue = .Cells.Find(What:="*", After:=Range("A1"), LookIn:=xlValues, _
LookAt:=xlPart, SearchOrder:=xlByColumns, SearchDirection:=xlPrevious)
Set RowFormula = .Cells.Find(What:="*", After:=Range("A1"), LookIn:=xlFormulas, _
LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlPrevious)
Set RowValue = .Cells.Find(What:="*", After:=Range("A1"), LookIn:=xlValues, _
LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlPrevious)
On Error GoTo 0
'Determine the last column
If ColFormula Is Nothing Then
LastCol = 0
Else
LastCol = ColFormula.Column
End If
If Not ColValue Is Nothing Then
LastCol = Application.WorksheetFunction.Max(LastCol, ColValue.Column)
End If
'Determine the last row
If RowFormula Is Nothing Then
LastRow = 0
Else
LastRow = RowFormula.Row
End If
If Not RowValue Is Nothing Then
LastRow = Application.WorksheetFunction.Max(LastRow, RowValue.Row)
End If
'Determine if any shapes are beyond the last row and last column
For Each Shp In .Shapes
j = 0
k = 0
On Error Resume Next
j = Shp.TopLeftCell.Row
k = Shp.TopLeftCell.Column
On Error GoTo 0
If j > 0 And k > 0 Then
Do Until .Cells(j, k).Top > Shp.Top + Shp.Height
j = j + 1
Loop
If j > LastRow Then
LastRow = j
End If
Do Until .Cells(j, k).Left > Shp.Left + Shp.Width
k = k + 1
Loop
If k > LastCol Then
LastCol = k
End If
End If
Next
.Range(.Cells(1, LastCol + 1), .Cells(.Rows.Count, .Columns.Count)).EntireColumn.Delete
.Range("A" & LastRow + 1 & ":A" & .Rows.Count).EntireRow.Delete
End With
Next
Application.ScreenUpdating = True
Application.DisplayAlerts = True
End Sub
Excel saves the UsedRange. In order to truncate the UsedRange, you need to delete whole rows and save the file.
If that's not an option, insert a new worksheet, copy the prepared data to it (thus leaving its UsedRange matching actual data), use Worksheet.SaveAs (as opposed to Workbook.SaveAs) and delete the worksheet.
Although the actual problem here is why your UsedRange gets that big in the first place.

Resources