I wrote a VBA program that is supposed to download and copy emails from my outlook account and paste them onto my excel spreadsheet. I would like to run this program daily so obviously, I don't want this to go through my entire mailbox every time. So rather, I want it to start searching for emails after the date of the last copied email. But when I try to run this, it doesn't work. It keeps going through the entire mailbox and loops backward. So for instance, it will look for 6/29/2015 email and then move on to 6/28/2015, 6/27/2015 and so on, which is the opposite of what I am trying to to accomplish. I am not sure what I am doing wrong. Any help would be extremely appreciated. Thanks in advance!
Sub Download_Outlook_Mail_To_Excel()
Dim Folder As Outlook.MAPIFolder
Dim iRow As Integer, oRow As Integer, fRow As Integer
Dim MailBoxName As String, Pst_Folder_Name As String
MailBoxName = "officework#gmail.com"
Pst_Folder_Name = "Inbox" 'Sample "Inbox" or "Sent Items"
Set Folder =
Outlook.Session.Folders(MailBoxName).Folders(Pst_Folder_Name)
If Folder = "" Then
MsgBox "Invalid Data in Input"
GoTo end_lbl1:
End If
ThisWorkbook.Sheets(1).Activate
Folder.Items.Sort "Received"
' Insert Column Headers
' ThisWorkbook.Sheets(1).Cells(1, "A") = "Sender"
' ThisWorkbook.Sheets(1).Cells(1, "D") = "Subject"
' ThisWorkbook.Sheets(1).Cells(1, "F") = "Date"
' ThisWorkbook.Sheets(1).Cells(1, "J") = "EmailID"
' ThisWorkbook.Sheets(1).Cells(1, "M") = "Body"
LastRow = Cells.Find(What:="*", SearchDirection:=xlPrevious, SearchOrder:=xlByRows).Row
vDate = Cells(LastRow, "F").Value
For fRow = 1 To Folder.Items.Count
If Folder.Items.Item(fRow).ReceivedTime >= vDate Then
For iRow = LastRow To Folder.Items.Count
oRow = iRow + 1
ThisWorkbook.Sheets(1).Cells(oRow, 1).Select
ThisWorkbook.Sheets(1).Cells(oRow, "A") = Folder.Items.Item(iRow).SenderName
ThisWorkbook.Sheets(1).Cells(oRow, "D") = Folder.Items.Item(iRow).Subject
ThisWorkbook.Sheets(1).Cells(oRow, "F") = Folder.Items.Item(iRow).ReceivedTime
ThisWorkbook.Sheets(1).Cells(oRow, "J") = Folder.Items.Item(iRow).SenderEmailAddress
ThisWorkbook.Sheets(1).Cells(oRow, "M") = Folder.Items.Item(iRow).Body
Next iRow
End If
Next fRow
MsgBox "Outlook Mails Extracted to Excel"
end_lbl1:
End Sub
I have noticed the following lines of code:
For fRow = 1 To Folder.Items.Count
If Folder.Items.Item(fRow).ReceivedTime >= vDate Then
It looks like you are iterating through each item in the folder and check the RecievedTime property. Iterating through all items in the folder is not a really good idea. So, I'd suggest using the Find/FindNext or Restrict methods of the Items class instead. The latter applies a filter to the Items collection, returning a new collection containing all of the items from the original that match the filter. So, you just need to iterate through a small number of items and do whatever you need without checking properties each time. You can read more about these methods in the following articles:
How To: Use Find and FindNext methods to retrieve Outlook mail items from a folder (C#, VB.NET)
How To: Use Restrict method to retrieve Outlook mail items from a folder
Dates and times are typically stored with a Date format, but the Find and Restrict methods require that the date and time be converted to a string representation. To make sure that the date is formatted as Microsoft Outlook expects, use the Format function (available in VBA):
sFilter = "[RecevedTime] > '" & Format("6/29/2015 9:30pm", "ddddd h:nn AMPM") & "'"
Also you may find the AdvancedSearch method of the Application class helpful. he key benefits of using the AdvancedSearch method in Outlook are:
The search is performed in another thread. You don’t need to run another thread manually since the AdvancedSearch method runs it automatically in the background.
Possibility to search for any item types: mail, appointment, calendar, notes etc. in any location, i.e. beyond the scope of a certain folder. The Restrict and Find/FindNext methods can be applied to a particular Items collection (see the Items property of the Folder class in Outlook).
Full support for DASL queries (custom properties can be used for searching too). You can read more about this in the Filtering article in MSDN. To improve the search performance, Instant Search keywords can be used if Instant Search is enabled for the store (see the IsInstantSearchEnabled property of the Store class).
You can stop the search process at any moment using the Stop method of the Search class.
See Advanced search in Outlook programmatically: C#, VB.NET for more information.
Finally, take a look at the Getting Started with VBA in Outlook 2010 article in MSDN which explains basics of Outlook programming.
I started my answer before Eugene posted his but finished later because I was trying to do other things in parallel. His answer provides good advice and your code will be better for following that advice. My code is much closer to yours so you might find it easier to understand at this stage in your development as a VBA programmer.
I think I know what is wrong with your code but first some general advice.
Placing Option Explicit at the top of a module means that all variables have to be properly declared. Some of your variables are defined but others are not. If you use a variable that has not been declared with a Dim statement, VBA will declare it for you as a Variant. A variable of type Variant can take any type of value which is slow and error prone. Consider:
X = 5
X = “A”
In the above code, I have not declared X so it is a Variant. Setting X to 5 then “A” is valid code with Variants. Placing these two statements together makes my mistake easy to spot. But if I use X throughout my macro, a statement that sets X to the wrong type of value may be difficult to spot and can lead to strange failures.
Dim X as Long
X = 5
X = “A”
Adding the Dim statement means the interpreter will reject X = “A” at runtime.
If you indent code within For-Next loops and If-Else-EndIf blocks, it becomes much easier to follow the code. I suspect this is one of your problems.
Your other problem is the use of meaningless names. You have iRow, oRow, fRow and LastRow. Perhaps there is a system behind these names but it does not appear so. If there is a system, will you remember that system when you need to update this macro in six or twelve months? I have a system of naming variables which means I can look at macros I wrote ten years ago and immediately know what all the variables are.
My variable names are all a sequence of words or abbreviations. The first word is always what I am using the variable for so “Row” means it’s a Row number. The next word defines what it is a row of. You have rows of an Excel sheet and an Outlook folder so perhaps “RowSht” and “RowFld” or “RowEx” and “RowOut”. Sometimes I need a fourth word but usually three is enough. For a row number, the third word will typically be “Crnt” (for current), First, Last or Next.
There are other general points I could make about your code but I think that is enough for now.
I believe the major problem is that you have both an outer and an inner loop through the rows of the folder. Your choice of variable names makes this difficult to identify,
Issue 1
Dim RcvdPrevLatest as Date
Dim RowShtCrnt as Long
RowShtCrnt = Cells.Find(What:="*", SearchDirection:=xlPrevious, SearchOrder:=xlByRows).Row
RcvdPrevLatest = Cells(LastRow, "F").Value
RowShtCrnt = RowShtCrnt + 1
You only need one variable for the rows of you worksheet so I have named it RowShtCrnt. I have declared it as Long because Integer declares a 16-bit variable which requires special processing on 32 and 64-bit computers and because Integer only allows for 64K rows which is less than the maximum number of rows unless you are using Excel 2003.
I have declared a variable for your date. My system forces me to think about the purpose of my variables. It holds a Received Date/Time so the first word is “Rcvd”. The particular Received Date/Time being held is the latest Received Date/Time from the previous run of the macro.
Issue 2
You have:
For fRow = 1 To Folder.Items.Count
If Folder.Items.Item(fRow).ReceivedTime >= vDate Then
For iRow = LastRow To Folder.Items.Count
The outer For-Loop searches down the folder for a mail item with a received date/time later than the previous latest. The inner For-Loop then outputs every mail item below it in the folder so you are outputting mail items repeatedly. The inner For-Loop starts iRow at the last row of the worksheet and continues to the last row of the folder. You choice of names makes it difficult to spot that the start and end values do not relate to one another. Since fRow is not used within the inner loop, its value has no effect on the mail items selected for output.
I have not tested this code but it will be closer to what you want:
Dim RowFldCrnt as Long
For RowFldCrnt = 1 To Folder.Items.Count
If Folder.Items.Item(RowFldCrnt).ReceivedTime > RcvdPrevLatest Then
ThisWorkbook.Sheets(1).Cells(RowShtCrnt, "A") = _
Folder.Items.Item(RowFldCrnt).SenderName
ThisWorkbook.Sheets(1).Cells(RowShtCrnt, "D") = _
Folder.Items.Item(RowFldCrnt).Subject
: : :
RowShtCrnt = RowShtCrnt + 1
Next
Related
First, I would like to apologize for my bad language, I hope you'll understand my problem.
I looked after a way to get generic function in Excel and I found the add-in method. So I tried to use it in developping custom functions whitch may help me in my everyday work. I developed a first function which work. So I thought that my add-in programmation and installation was good. But when I try to implement worksheet interractions nothing appened.
My code has to delete rows identified by a special code in a cell of those ones. I get no error message and the code seems to be totally executed. I tried other methods like Cells.delete, Cells.select, worksheet.activate or range.delete but I encounter the same issue.
This is my function's code :
Public Function NotBin1Cleaning(rSCell As Range) As Integer
Dim sht As Worksheet
Dim aLine As New ArrayList
Dim iLine As Integer
Dim iCpt As Integer
Dim iFail As Integer
Dim i As Integer
Dim oRange As Object
Set sht = rSCell.Parent
iLine = sht.Cells.Find("*PID*").Row
For Each rCell In Range(sht.Cells(iLine, 1), sht.Cells(sht.Cells(iLine, 1).End(xlDown).Row, 1))
If sht.Cells(rCell.Row, 2) > 1 Then
iLine = rCell.Row
iCpt = iLine + 1
Do Until sht.Cells(iCpt, 2) = 1
If Not sht.Cells(iCpt, 1) = rCell Then Exit Do
iCpt = iCpt + 1
Loop
If sht.Cells(iCpt, 1) = rCell Then
sht.Range(sht.Cells(iLine, 1), sht.Cells(iCpt - 1, sht.Cells(iCpt, 1).End(xlToRight).Column)).Delete xlUp
iFail = iFail + 1
End If
End If
Next
NotBin1Cleaning = iFail
End Function
it's the line:
sht.Range(sht.Cells(iLine, 1), sht.Cells(iCpt - 1, sht.Cells(iCpt, 1).End(xlToRight).Column)).Delete xlUp
which isn't producing any effect.
I would be really thankful for your help.
This issue is described on the Microsoft support site as part of the intentional design
section below, more detail here (emphasis mine)
A user-defined function called by a formula in a worksheet cell cannot change the environment of Microsoft Excel. This means that such
a function cannot do any of the following:
Insert, delete, or format cells on the spreadsheet.
Change another cell's value.
Move, rename, delete, or add sheets to a workbook.
Change any of the environment options, such as calculation mode or screen views.
Add names to a workbook.
Set properties or execute most methods.
The purpose of user-defined functions is to allow the user to create a
custom function that is not included in the functions that ship with
Microsoft Excel. The functions included in Microsoft Excel also cannot
change the environment. Functions can perform a calculation that
returns either a value or text to the cell that they are entered in.
Any environmental changes should be made through the use of a Visual
Basic subroutine.
Essentially, this means that what you're trying to do won't work in such a concise manner. The limitation, as I understand from further reading, is because Excel runs through cell equation/functions several times to determine dependencies. This would lead to your function being called two or more times. If you could delete rows, there is the potential of accidentally deleting more then twice the numbers of rows intended, due to the excess number of runs.
However, an alternative could be to have the function output a unique string result that shouldn't be found anywhere else in your workbook (maybe something like [#]><).
Then you can have a sub, ran manually, which finds all instances of that unique string, and deletes those rows. (Note: if you included any of the typical wildcard symbols in your string, you will have to precede them with a ~ to find them with the .Find method.) You can even set up the sub/macro with a shortcut key. Caution: if you duplicate a shortcut key Excel already uses, it will run the macro instead of the default. If there will be other users using this workbook, they could experience some unexpected results.
If you decide to go this route, I would recommend using this line:
Public Const dummy_str = "[#]><" ' whatever string you decided on.
in your module with your code. It goes outside any functions or subs, so it'll be global, and then you can refer to the const just as you would any other string variable.
When you write:
sht.Range(sht.Cells(iLine, 1),....
This first parameter should be the row number, but you're refering to a Cell instead. You should change sht.Cells(iLine, 1) for iLine.
BUT
Instead of all this, its easier to use the method Row.Delete:
Rows(iLine).EntireRow.Delete
The following is the result of downloading information from an accounting system. Basically, I was tasked with sorting through expenses from this year from an online system; once the information was downloaded from the online system, it was not formatted as a spreadsheet (so I couldn't easily use a simple lookup). The information was downloaded as a spreadsheet, however it didn't contain check numbers or names; excel formatted those away for some reason. The only thing that was left is the long stringy document, where each item in the PDF downloaded (which contained check numbers and names) was placed in column 1 (see picture 1), whereas it should have been placed in something formatted like picture 2. Obviously though PDF's do not maintain formatting.
So baring some way that I can transfer the PDF to a workbook and run an analysis (IE through copy paste or save as) I needed to get information from this long stringy thing (it's at 9000 rows at the moment, added in an excerpt).
First, this code sets the worksheet pers as a worksheet, gets the length of data in pers (example in picture 2), and length of data in expensesheet (example in picture 1)
Then it scans pers for items (prior to writing this code items were added manually - such as in the case of picture 2, 'supply 1' and corresponding information that can help denote supply 1, ie invoice #, description, date cut, and so on).
For each of those items, it then scans the "expense sheet". It tries to match the invoice number (which is the closest thing to a unique ID in this case) to the value in cell i, 1; if it exists, it then scans 'upwards' until it finds a long enough string so that it can be the 5 unit string; the one that contains a date, a check number, an amount, and a name, as well as a batch number and a memo.
Once it finds that string, it then splits it into an array, and then seeks to place it in the corresponding cells to the right of that row in worksheet pers.
Issues:
1) I keep receiving an error 400. Normally when I receive an error VBA shows what line. What is this? How can I set up an error catching block so that the editor will provide me more details on the error (ie place it occurred, reason for occurrence, etc)
2) I'm assuming that the long row (in this case its 12th from the top) can only be identified through its length. Is there a better way to identify the long row? Perhaps if it contains multiple dashes?
3) Does anyone know of a way to easily transfer a PDF of an accounting printout so that it retains its formatting when saved or copied to a spreadsheet?
4) Is there a way that this spreadsheet could be easily formatted through excel so that it can more adequately fit into the proper mold (more like picture 2)?
Option Explicit
Sub findDetailMemo()
Dim pers As Worksheet
Set pers = ThisWorkbook.Sheets("PERS")
Dim persLength As Long
persLength = pers.Range("a1").End(xlDown).Row
Dim expenseLength As Long
expenseLength = Range("a1").End(xlDown).Row
Dim currentDetail() As String
Dim i As Long
Dim j As Long
Dim k As Long
Dim tempInt As Long
'first scan all of the items in the pers unit
For k = 2 To 10
'next scan all of the expenses
For i = 2 To expenseLength
'if the invoice # is found
If InStr(Cells(i, 1), pers.Range("a1").Offset(k, 3)) <> 0 Then
'scan upwards; make sure you don't scan beyond the range of the spreadsheet
For j = i To 1 Step -1
'if the scan upwards finds a string that is 80 characters or more
If Len(Cells(i - j, 1)) >= 80 Then
'split it at the -
currentDetail = Split(Cells(i - j, 1), "-", -1, vbTextCompare)
'add it to the pers sheet
pers.Range("a1").Offset(k, 11) = currentDetail(0)
pers.Range("a1").Offset(k, 12) = currentDetail(1)
pers.Range("a1").Offset(k, 13) = currentDetail(2)
pers.Range("a1").Offset(k, 14) = currentDetail(3)
Exit For
End If
Next j
Exit For
Else
End If
Next i
Next k
End Sub
EDIT: After a discussion through the chat lobby, bdpolinsky and I found what was throwing the original error 400 (which was actually error 1004).
The first issue we fixed was the InStr() and Split() functions were referencing Cell objects instead of the string within them. This was fixed by simply adding Cells().Text where strings were required.
On the line If Len(Cells(i - j, 1).Text) >= 80, we discovered that Cells() wasn't referencing the correct worksheet. The fix for this was to define Cells() as pers.Cells(), which is the worksheet the information was imported to. Happy to report that the problem bdpolinsky was having has been solved (as far as the errors go).
The following is from the original answer:
1) At the start of your code (first executable line) you can press F8 to step through the code 1 line at a time until the error is flagged.
You can also use error handlers to catch an error and have excel do something different than default. Error Handling
Sub SomeCode()
Dim i As Integer
On Error GoTo ErrHandler
i = 1/0
ErrHandler:
MsgBox "Error Description: " & Err.Description
End Sub
You can also click next to a line of code to add a Break. Breaks look like red circles, and color that line of code red. Your code will stop when it reaches this line.
2)If Len(cellThatYoureChecking) > 20 Then Code
Or
If InStr(cellThatYoureChecking, "symbolYouWantToFind") <> 0 Then Code
Or visit this post about defining how many times a character is in a string with a function. You could then make your If statement based on the number of times it occurs.
3) This part is poor form for StackOverflow, but what you're asking is a little involved so see if this tutorial is of use to you. Import table from PDF to Excel.
4) The short answer to this is yes. There are a lot of ways to reorganize data in Excel. This question is a little too broad though, and it'd be more efficient to get questions 1-3 answered first before getting too ahead of ourselves.
So I made a simple user form that will allow people to sign out equipment easily. I would like to make it so if something in the "Equipment" column is out, it will say out in the "In/Out" column. But otherwise say in. So if Equipment says "Laptop 1" and Last "Date & Time" Column is empty, then it would say out beside Laptop 1 in the "IN/OUT" column. The equipment column is multiselect with a "," between equipment options.
I have no idea where to start with this. What I have done so far minus populating the listbox and dropdown for the user form entry.
Private Sub cmdout_Click()
Set ws = ThisWorkbook.Worksheets("SignOut")
Dim sh As Worksheet
Dim LastRow As Long
Dim i As Integer
For i = 0 To equip.ListCount - 1
If equip.Selected(i) Then
Msg = Msg & equip.List(i) & ", "
End If
Next i
Msg = Left(Msg, Len(Msg) - 2)
Dim rngFound As Range
Dim strFirst As String
Dim strID As String
Dim strDay As String
Dim taken As Integer
strID = gov.Value
strDay = ""
Set rngFound = Columns("C").Find(strID, Cells(Rows.Count, "C"), xlValues, xlWhole)
If Not rngFound Is Nothing Then
strFirst = rngFound.Address
Do
If LCase(Cells(rngFound.Row, "G").Text) = LCase(strDay) Then
MsgBox "GOV is still signed out."
taken = 1
End If
Set rngFound = Columns("C").Find(strID, rngFound, xlValues, xlWhole)
Loop While rngFound.Address <> strFirst
End If
If taken = 0 Then
Application.Worksheets("SignOut").Range("A" & Rows.Count).End(xlUp).Offset(1).Value = Now()
Application.Worksheets("SignOut").Range("B" & Rows.Count).End(xlUp).Offset(1).Value = techname.Value
Application.Worksheets("SignOut").Range("C" & Rows.Count).End(xlUp).Offset(1).Value = gov.Value
Application.Worksheets("SignOut").Range("D" & Rows.Count).End(xlUp).Offset(1).Value = Msg
Application.Worksheets("SignOut").Range("E" & Rows.Count).End(xlUp).Offset(1).Value = otherequip.Value
End If
Set rngFound = Nothing
End Sub
Sign in form:
Private Sub CommandButton1_Click()
Dim rngFound As Range
Dim strFirst As String
Dim strID As String
Dim strDay As String
strID = techname1.Value
strDay = ""
Set rngFound = Columns("B").Find(strID, Cells(Rows.Count, "B"), xlValues, xlWhole)
If Not rngFound Is Nothing Then
strFirst = rngFound.Address
Do
Application.Worksheets("SignOut").Cells(rngFound.Row, "G").Value = Now()
Set rngFound = Columns("B").Find(strID, rngFound, xlValues, xlWhole)
Loop While rngFound.Address <> strFirst
End If
Set rngFound = Nothing
End Sub
1) A "form" is a specific coding construct in VBA. Based on what you posted, I assume that you aren't referring to it, but instead are just calling this sheet a form? If not, then please post the VBA code that you have tried.
2) Assuming that the list of equipment is in the "Tracker" column, you should use that list to populate a dropdown list in the equipment column to ensure that they match. I am also assuming that your extra equipment column won't have anything in the dropdown list and if people check out 2 tracked items, there will be a line entry for each item. (I'd recommend getting rid of that column if you get users that misuse it)
3) Since you asked where to start, I'll give you that. You'll learn much more by figuring out the exact syntax yourself. You can google "Excel VBA X" where X is basically any of these lines.
Pseudocode - (will not run, needs to be replaced with actual code - also ignore the colored words, they don't mean anything in pseudocode)
Phase 1:
trigger event on save (event handler is another search term for trigger events)
Change all equipment values to In
loop through first date/time column
IF there is a value in that column and there is not a value in the second date/time column get the name of the equipment from the equipment column
Find equipment from tracker column change In/Out value on that row to Out
continue the loop until the next row is blank
Alternate:
remove code to check everything in
add on-edit trigger to equipment column
add row that was edited to array
add on-edit trigger to check in date column
store row number to array
change loop so it only goes through rows in array
change if so that if something is checked out but not in, it is set out
(You will want to do this in case someone selects the wrong thing and then changes it - don't change it to out immediately or you will need logic to realize what was changed to out the previous time and change it back to in.)
else if something is checked out and has a value in check in date column then set it to in
Phase 2:
Implement an actual form that people use to fill in the sheet and check things in and out
Reuse relevant code from above but eliminate human error on dates and other things
(I suggest this as phase 2 as you can do this without a form and you will be using less new code. I would definitely use a form myself but it would be better if you wade into the pool instead of diving in. Unless you have coding experience and just need to learn syntax and vocab, then dive away.)
There are a lot of other things I would do if this was a form I was making, but this should get you started in terms of what to search for to build this project. I tried to make it as simple as possible so that it isn't overwhelming. There are better ways to do it but these methods should be ones that you can grasp quickly and then improve upon later after you learn more. Good luck with your equipment tracking!
Edit: after code posted
Ok, with the code you posted, go all the way to the top before the sub line and put in:
Option Explicit
This will cause the VBE editor to give you more meaningful feedback in quite a few instances. For example, you have your set line before your dim line. Without Option Explicit, when the editor comes to a variable that has not been declared with a Dim statement, it just makes it on the fly and sets it as a variant type. That uses up extra memory and means that typos get variables created on the fly. So when you are doing what you have done here, you end up with
Dim sh As Worksheet ' your sh variable is your worksheet variable. It never gets used again.
Set ws = ThisWorkbook.Worksheets("SignOut")' the ws here should likely be sh to match the Dim statement ... or the sh in the Dim should be a ws. Except it doesn't ever get used again either.
Neither of those matter in this case since you aren't reusing them but if you had code that was referring to one or the other, you would want the compiler to tell you that you are trying to use a variable that hasn't been declared instead of creating a new one.
Generally you want to put your Dim statements all at the top of the sub or function. 1) It is easier to find them and debug or check spelling. 2) It ensures that they are all declared before the script tries to reference them.
Your code here isn't populating the variables before it is referencing them. Pretty much any time that you have a variable you need to populate it before you can do anything with it. There are a number of ways to populate variables with data from the sheet. If you get comfortable with arrays sooner rather than latter (or collections instead of arrays) then you will have a much easier time with a task like this.
Some specific lines:
Dim LastRow as Long 'you have this declared but you need to put in code to get the last row, which will be handy for populating variables later in your code. Do this right after declaring variables. Google excel vba find last row.
For i = 0 To equip.ListCount - 1 ' you need to populate equip before doing this. Lookup excel vba how to copy a range into variable. Then lookup how to loop through range. You can start it at your first line of data and go down to the LastRow to grab them all into the same array (which will basically be a table).
msg = Left(msg, Len(msg) - 2) 'lookup excel vba string manipulations if this isn't doing what you want
'these next lines all have <Variable Name>.value which won't work for you. If those variables are supposed to be things that the sheet prompts someone to enter and then they get populated here you can see how to do that here-> http://www.excel-vba-easy.com/vba-userform-excel-vba.html
Application.Worksheets("SignOut").Range("B" & Rows.Count).End(xlUp).Offset(1).Value = techname.Value
Application.Worksheets("SignOut").Range("C" & Rows.Count).End(xlUp).Offset(1).Value = gov.Value
Application.Worksheets("SignOut").Range("D" & Rows.Count).End(xlUp).Offset(1).Value = msg
Application.Worksheets("SignOut").Range("E" & Rows.Count).End(xlUp).Offset(1).Value = otherequip.Value
With your loop untils you will want to ensure that you test with a short loop and step through it over and over. If you do the logic wrong and end up looping infinitely you can easily stop stepping through and fix it (or fix it while stepping through, then stop and retest,) but if you just hit play then excel will freeze up on you.
If you run into an issue with a specific step you can probably find lots of existing things on SO. If not, post a new thread with the specifics of that step. This posting will be down a few pages by then and people likely won't see your question if you put it here. Also, it will deserve its own thread since you will have moved past the "where to start" stage at that point.
Good luck!
Hello everyone alright let start by giving some brief background on my project then I will follow up with my specific issue and code.
Currently I am building a program to automate the process of filling a template. This template exceeds 60,000 rows of data quite often and I've built the large majority of it to work month to month by plugging in new data sheets and running it. Currently all of the work is based off of one data sheet which I import into excel manually. This data sheet does not contain all the data I need to populate the template so now I am beginning to bring in additional data to supplement this. The problem herein lies with data association. When I was originally pulling from one data sheet I didn't have to worry if the data I pulled for each row coincided with the other rows because it all came from the same sheet. Now I have to cross check data across two sheets to confirm it is pulling the correct information.
Now for what you need to know. I am trying to fill a column that will be referred to as Haircut, but before I do that I need to confirm that I am pulling the correct haircut number in correlation to a Trade ID which was already populated into the template in a previous line of code.
Using similar logic that I have been using throughout my entire project this is a snippet of code I have to perform this task.
Dim anvil as Worksheet
Dim ALLCs as worksheet
Dim DS as worksheet
'''''''''''''''''''''''''''''code above this line is irrelevant to answer this question
ElseIf InStr(1, DS.Cells(x, 2), "Haircut") Then
Anvil.Select
For y = 1 To 80
If Anvil.Cells(1, y) = "Haircut" Then
For Z = 1 To 80
If Anvil.Cells(1, Z) = "Trade ID" Then
For t = 2 To 70000
For u = 16 To 70000
If Anvil.Cells(t, Z) = ALLCs.Cells(u, 34) Then
ALLCs.Cells(u, 27) = Anvil.Cells(t, y)
End If
Next
Next
End If
Next
End If
Next
This code coupled with my other code I assume will in theory work, but I can only imagine that it will take an unbelievable amount of time(this program already takes 7 and a half minutes to run). Any suggestions on how to rewrite this code with better functionality, following this general logic?
Any help is appreciated, whether you completely revamp the code, or if you offer suggestions on how to cut down loops. I am also looking for suggestions to speed up the code in general aside from screen updating and calculation suggestions.
If I understand the logic correctly then you can replace all but one of the loops with a .Find() method like so:
'// Dimension range objects for use
Dim hdHaricut As Excel.Range
Dim hdTradeID As Excel.Range
Dim foundRng As Excel.Range
With Anvil
With .Range("A1:A80") '// Range containing headers
'// Find the cell within the above range that contains a certain string, if it exists set the Range variable to be that cell.
Set hdHaircut = .Find(What:="Haircut", LookAt:=xlWhole)
Set hdTradeID = .Find(What:="Trade ID", LookAt:=xlWhole)
End With
'// Only if BOTH of the above range objects were found, will the following block be executed.
If Not hdHaricut Is Nothing And Not hdTradeID Is Nothing Then
For t = 2 To 70000
'// Using the .Column property of the hdTradeID range, we can see if the value of Cells(t, hdTradeColumn) exists
'// in the other sheet by using another .Find() method.
Set foundRng = ALLCs.Range(ALLCs.Cells(16, 34), ALLCs.Cells(70000, 34)).Find(What:=.Cells(t, hdTradeID.Column).Value, LookAt:=xlWhole)
'// If it exists, then pass that value to another cell on the same row
If Not foundRng Is Nothing Then ALLCs.Cells(foundRng.Row, 27).Value = .Cells(t, hdHaircut.Column).Value
'// Clear the foundRng variable from memory to ensure it isn't mistaken for a match in the next iteration.
Set foundRng = Nothing
Next
End If
End With
I am working with a workbook that contains three worksheets of data. Each worksheet has a Contract Number column. Certain contracts must be excluded and noted in a separate worksheet.
I would like to create Excel VBA macro that:
Prompts the user to enter specific contract numbers to be excluded
Stores contract numbers
Searches all three worksheets' contract column for the contract numbers
Notes the unwanted contract details in a "summary" worksheet, which has already been created
Deletes the unwanted contract row entirely
The macro should loop through this process below for 'n' number of contracts entered by the user.
Public contString As String
Public x As Variant
Public xCount As Variant
Sub find()
contString = InputBox(Prompt:="Enter contract numbers to exclude(Comma Delimited). Cancel to include all contracts.", _
Title:="Exclude Contracts", Default:="1715478")
x = Split(contString, ",")
xCount = UBound(x) 'Number of contracts entered by user
End Sub
Sub SearchWS1()
Sheets("WS1").Activate
Columns("I:I").Select 'Contract Number Column
Selection.find(What:=x(i), After:=ActiveCell, LookIn:=xlValues, _
LookAt:=xlPart, SearchOrder:=xlByColumns, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False).Activate
BKWS = ActiveCell.Worksheet.Name
BKRow = ActiveCell.Row
If BKRow > 0 Then
Cname = Range("G" & BKRow)
Cnumber = Range("I" & BKRow)
Cvalue = Range("K" & BKRow)
'Summarize Excluded Contract Info on Summary WS
Range("Summary!B25").Value = "Exclusions:"
Range("Summary!B26").Value = Cnumber
Range("Summary!C26").Value = Cname
Range("Summary!D26").Value = Cvalue
'Select and Delete Contract
Rows(ActiveCell.Row).Select
Rows(BKRow).EntireRow.Delete
Else
Call SearchWS2 'SearchWS2 is essentially the same as SearchWS1 and Calls SearchWS3 if contract isn't found.
End If
End Sub
If the contract number doesn't exist in the first WS, I get an error like 'Object variable or With block not set'. Once I can fix this error, I will need to run this process through a loop for each contract number entered by the user. Any help with debugging the error or setting up a loop for this would be greatly appreciated.
Thanks!
Use the InputBox for inputting contract numbers (let's say, comma delimited). Split the result using Split function.
Store contract numbers on a separate worksheet that you hide (wks.visible=xlVeryHidden, where wks is a worksheet object).
Find values using a multidimensional array to store the values.
Print 2D array to found worksheet using rFound=saArray (where rFound is a range object and saArray is the 2D array.
Make heavy use of recording macros to learn syntax.
See this example on fast ways to retrieve and print to cells.
Update:
Sorry, this is pretty sloppy but I just threw it together and, obviously, it hasn't been tested. Hope this helps. Sorry, I also shouldn't be having you use advanced techniques like this, but it's hard for me to go back.
dim j as integer, k as integer, m as long, iContractColumn as integer
Dim x() as string, saResults() as string
dim vData as variant
dim wks(0 to 2) as worksheet
iContractColumn=????
set wks(0) = Worksheets("First")
set wks(1) = Worksheets("Second")
set wks(2) = Worksheets("Third")
redim saresults(1 to 100, 1 to 2)
m=0
'Loop thru worksheets
for j=0 to 2
'Get data from worksheet
vdata=wks(j).range(wks(j) _
.cells(1,iContractColumn),wks(j).cells(rows.count,iContractColumn).end(xlup))
'Loop through data
for k=1 to ubound(vdata)
'Loop through user criteria
For i = 0 To UBound(x)
'Compare user criteria to data
if x(i)=cstr(vdata(k,1)) then
'Capture the row and worksheet name
m=m+1
'If array is too small increase size
if m>ubound(saresults) then
redim preserve saresults(1 to ubound(saresults)*2, 1 to 2)
end if
'Get name and row.
saresults(m,1)=wks(j).name
saresults(m, 2)=k
exit for
end if
next i
next k
next j
'Resize array to correct size
redim preserve saresults(1 to m, 1 to 2)
'Print results to a result page (you could also create hyperlinks here
'that would make it so the person can click and go to the respective page.
'You would have to do a loop for each result on the range.
with worksheets("Result Page")
.range(.cells(1,1),.cells(m,2))=saresults
end with
I have little to add Jon49's answer which does seem to cover the basics. But I wish I had discovered Forms earlier in my VBA programming career. They can be a little confusing at first but, once mastered, they add enormously to the usability of a macro for very little effort.
Forms can be used to get values from the user (instead of InputBox) or can be used to give progress information to the user. I will only talk about the second usage. Your macro might take some time; has the user time to get a cup of coffee or will it finish in 5 seconds? I HATE programs that sit there saying "please wait - this may take from a few minutes to a few hours".
The following code loads a form into memory, shows it to the user and removes it from memory at the end. If you do not unload the form, it remains on the screen after the macro has ended which may be useful if you want to leave a message for the user. This form is show "modeless" which means the macro displays it and carries on. If shown "modal", the macro stops until the user has entered whatever information the form requires.
Load frmProgress
Progress.Show vbModeless
' Main code of macro
Unload frmProgress
There are no end to the web sites offering tutorials on Forms so I will mainly describe the what rather than how.
Within the VB Editor, Insert a UserForm. Drags the bottom and right edges if you want it bigger. Use the Properties Window to change the Name to frmProgress.
Drag four labels from the Tool Box and arrange them in a line. Set the caption of label 1 to "Worksheet " and the caption of label 3 to "of". Name label 2 "lblWSNumCrnt" and name label 4 "lblWSNumTotal".
Add the following around "for j = 0 to 2"
frmProgress.lblWSNumTotal.Caption = 3
for j = 0 to 2
frmProgress.lblWSNumCrnt.Caption = j + 1
DoEvents
This means the user will see the following with n stepping from 1 to 3 as the macro progesses:
Worksheet n of 3
Add another four labels for row number, and the following code around the k loop:
frmProgress.lblRowNumTotal.Caption = ubound(vdata, 1)
for k = 1 to ubound(vdata, 1)
frmProgress.lblRowNumCrnt.Caption = k
DoEvents
Now the user will see something like:
Worksheet 2 of 3
Row 1456 or 2450
The above technique is simple and does not involve any change to Jon49's code. The following technique, borrowed from Wrox's excellent Excel VBA Programmer's Reference, is a little more complicated but gives your macro a more professional appearance.
Create a label that runs across the entire form. Name it "lblToDo" and colour it white. Create another label of the same size over the top. Name it "lblDone" and colour it black.
Create a copy of the code to count the rows in each sheet at the top so you can calculate the total number of rows, "TotalRowsTotal", before you do anything else.
Create a new variable "TotalRowsCrnt", initialise it to zero and add one to it for every row in every worksheet.
Within the inner loop, add:
frmProgress.lblToDo.Width = _
frmProgress.lblDone.Width * TotalRowsCrnt / TotalRowsTotal
For Excel 2003, which all the organisations I work with still use, this gives a progress bar with the black Done label steadily covering the white ToDo label. Later version of Excel may offer a progress bar control as standard.
I hope this gives you some ideas for making your macros more attractive to your users.