VBA Excel odd behaviour - excel

I've been attempting to write a module that can be imported into any spreadsheet and adapted to manage user account information stored in any sheet within the workbook.
So far I've written the following module:
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' modUserSession
'
' The purpose of this module is to provide a layer that manages
' user accounts.
'
' There is a standard format that must be adheired to, this is
' done by populating the global SessionParams type with the
' relevant information describing the particular sheet where
' the user account information is stored.
'
Type SessionParameters
sheetName As String ' Name of the sheet that contains user accounts
sheetUnlock As String ' String to lock/unlock the users sheet
startRow As Integer ' Row on which user accounts begin
nameColumn As Integer ' Column on which user names are stored
passColumn As Integer ' Column on which user passwords are stored
forenameColumn As Integer ' Column where user forename is stored
surnameColumn As Integer ' Column where user surname is stored
telephoneColumn As Integer ' Column where user telephone number is stored
emailColumn As String ' Column where user email address is stored
End Type
Type UserAccount
userName As String
userPass As String
foreName As String
surName As String
telNum As String
emailAddr As String
End Type
Private SessionParams As SessionParameters
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' setSessionParams()
'
' Populates the global SessionParams that is used to reference the
' relevant sheet where user accounts are stored.
'
Public Function setSessionParams( _
ByRef sParams As SessionParameters _
)
SessionParams.sheetName = sParams.sheetName
SessionParams.sheetUnlock = sParams.sheetUnlock
SessionParams.startRow = sParams.startRow
SessionParams.nameColumn = sParams.nameColumn
SessionParams.passColumn = sParams.passColumn
SessionParams.forenameColumn = sParams.forenameColumn
SessionParams.surnameColumn = sParams.surnameColumn
SessionParams.telephoneColumn = sParams.telephoneColumn
SessionParams.emailColumn = sParams.emailColumn
End Function
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' nextEmptyUserRow()
'
' Will return the next empty row on the sheet referenced by
' the global SessionParams type.
'
Public Function nextEmptyUserRow()
Dim intRow As Integer
Dim sheetName As String
Dim nameColumn As Integer
sheetName = SessionParams.sheetName
nameColumn = SessionParams.nameColumn
intRow = SessionParams.startRow
While (True)
If (ActiveWorkbook.Sheets(sheetName).Cells(intRow, nameColumn) = "") Then
nextEmptyUserRow = intRow
Exit Function
End If
intRow = (intRow + 1)
Wend
End Function
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' findUserAccount()
'
' Finds the row on which a particular user account is stored and
' returns the row number.
'
' If the specified user account isn't found then 0 is returned.
'
Public Function findUserAccount( _
ByVal userName As String _
) As Integer
Dim intRow As Integer
Dim sheetName As String
Dim nameColumn As Integer
sheetName = SessionParams.sheetName
nameColumn = SessionParams.nameColumn
intRow = SessionParams.startRow
While (True)
If (ActiveWorkbook.Sheets(sheetName).Cells(intRow, nameColumn) = "") Then
findUserAccount = 0
Exit Function
End If
If (ActiveWorkbook.Sheets(sheetName).Cells(intRow, nameColumn) = userName) Then
findUserAccount = intRow
Exit Function
End If
intRow = (intRow + 1)
Wend
End Function
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' loadUserAccountByRow()
'
' Loads a user account from the specified row - even if the row is
' empty and contains no data, the cells are loaded and returned
' in a SessionParameters type and will be empty.
'
Public Function loadUserAccountByRow( _
ByVal intRow As Integer _
) As UserAccount
Dim sheetName As String
sheetName = SessionParams.sheetName
loadUserAccountByRow.userName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.nameColumn)
loadUserAccountByRow.userPass = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.passColumn)
loadUserAccountByRow.foreName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.forenameColumn)
loadUserAccountByRow.surName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.surnameColumn)
loadUserAccountByRow.telNum = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.telephoneColumn)
loadUserAccountByRow.emailAddr = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.emailColumn)
End Function
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' loadUserAccountByName()
'
' Loads the specified user account and returns a SessionParameters
' type with the loaded info.
'
' If the specified account doesn't exist then all of the fiels in
' the SessionParameters type will be empty.
'
' This function works by calling findUserAccount(), passing the
' specified userName as a parameter.
'
' If the account doesn't exist then findUserAccount() will return
' 0, in which case an empty row is loaded.
'
' If the account does exist it is loaded via a call to the
' loadUserAccountByRow() function.
'
Public Function loadUserAccountByName( _
ByVal userName As String _
) As UserAccount
Dim intRow As Integer
' Get the row on which the user account is stored - the
' findUserAccount function will return 0 if the specified
' user account does not exist.
'
intRow = findUserAccount(userName)
If (intRow = 0) Then
' Return an empty SessionParameters structure.
'
loadUserAccountByName = loadUserAccountByRow(nextEmptyUserRow)
Else
loadUserAccountByName = loadUserAccountByRow(intRow)
End If
End Function
The idea now is that you populate the global SessionParameters variable (SessionParams defined in the module) with information relevant to the particular sheet where the data is stored. I basically need to fill the SessionParams to indicate the sheet name and the start row/columns where the data is stored.
I have the following sheet:
So, what I'm doing now is testing the loadUserAccountByName() function (from the above module).
So first I have to initialise the global SessionParams with the relevant info that points to the sheet and defines where on the sheet everything is.
So when my workbook is first opened I have the following code being executed:
Private Function populateSessionParams()
Dim sParams As SessionParameters
Dim userRow As Integer
sParams.sheetName = "Users"
sParams.sheetUnlock = "UnlockSheet"
sParams.startRow = 5
sParams.nameColumn = 4
sParams.passColumn = 6
sParams.forenameColumn = 8
sParams.surnameColumn = 10
sParams.telephoneColumn = 12
sParams.emailColumn = 14
setSessionParams sParams
End Function
Private Sub Workbook_Open()
Dim uParams As UserAccount
populateSessionParams
uParams = loadUserAccountByName("Admin")
MsgBox _
"Username: " & uParams.userName & vbCrLf & _
"Password: " & uParams.userPass & vbCrLf & _
"Forename: " & uParams.foreName & vbCrLf & _
"Surname: " & uParams.surName & vbCrLf & _
"Tel: " & uParams.telNum & vbCrLf & _
"Email: " & uParams.emailAddr
End Sub
In the Workbook_Open() function I first call the populateSessionParams() function - this initialises the global SessionParams defined in the module with appropriate values.
Everything works great except for one thing - the loadUserAccountByName() function calls on the loadUserAccountByRow() function to populate the UserAccount type with the relevant info if it finds a particular user account. And it is finding the Admin account and loading all of the relevant data except for the emailAddr member which it refuses to populate.
The error can be traced to the following line from the loadUserAccountByRow() function:
loadUserAccountByRow.emailAddr = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.emailColumn)
I can see from the debugger that SessionParams.emailColumn definitely contains the value 14 which is the column where email addresses are stored in the Users sheet. I can also see in the debugger that the value of sheetName is indeed "Users"
It populates all other values of the UserAccount type using pretty much the same code:
' Each of the following lines work.
loadUserAccountByRow.userName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.nameColumn)
loadUserAccountByRow.userPass = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.passColumn)
loadUserAccountByRow.foreName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.forenameColumn)
loadUserAccountByRow.surName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.surnameColumn)
loadUserAccountByRow.telNum = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.telephoneColumn)
' This particular line will not work and I get an application or
' object defined error. If I comment this line out, everything works
' great and there are no errors.
'
loadUserAccountByRow.emailAddr = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.emailColumn)
I've been beating my brow over this for a bit now and can't seem to get my head around it. I know it's a bit long winded but you really need all of the code and a reasonable explanation of what I'm trying to do, hope it makes sense.
Any help appreciated.

SessionParameters.emailColumn is the only one defined as String: all other column positions are Integer.
That is mostly likely the source of the error.

Related

using VBA to open webpage and pull specific line of data then place it in cell in excel

what I am trying to do is use VBA code, or any way actually to lookup a website and pull a CPU model found by using the Id I assume. I then need it to take that CPU model and Paste it into the Corresponding Cell in excel.
here are my examples.
I need the website https://partsurfer.hpe.com/Search.aspx?SearchText= to be pulled up with the cell from S/N to be appended right after the =
it then pulls a website with the hardware information. and line 33 is the cpu model. I need that populated into the CPU cell corresponding to the Serial number.
a serial number to test with MXQ2040F21
Here's a version that combines the cell iteration and the more robust web fetch from my prior two attempts.
Sub get_computer_data()
' be sure to set these two constants and a variable
Dim SheetName As String
SheetName = "computers" ' name of the sheet that has the computer data
Dim serialNumCol As Byte
serialNumCol = 4 ' the number of the column that has the serial number
Dim r As Long: r = 2 ' the row the computer data starts on
Dim s As Worksheet
Set s = ThisWorkbook.Worksheets(SheetName) 'process a specific sheet
' process all rows of contiguous data
Do Until s.Cells(r, 1).Value = ""
s.Cells(r, serialNumCol + 2).Value = get_processor(s.Cells(r, serialNumCol).Value)
r = r + 1
Loop
End Sub
Function get_processor(serial_number) As String
Dim position As Long
Dim search As String
Dim processor As String
Dim html As String
Const url = "https://partsurfer.hpe.com/Search.aspx?SearchText="
Dim xmlhttp As Object
Set xmlhttp = CreateObject("MSXML2.serverXMLHTTP")
xmlhttp.Open "POST", url & serial_number, False
xmlhttp.Send
html = xmlhttp.responseText
' find the tag that idenifies the processor in the html
search = "ctl00_BodyContentPlaceHolder_gridCOMBOM_ctl34_lblpartdesc1"">"
position = InStr(1, html, search) + Len(search)
if position = 0 then
get_processor = "not found"
else
processor = Split(Mid(html, position), "<")(0)
get_processor = processor
end iff
End Function
Here's an approach to get get the data you are after using a more direct method than the web query method in my other answer. This function takes a serial number and returns the processor.
Sub test()
Debug.Print get_processor("MXQ2040F21")
End Sub
Function get_processor(serial_number) As String
Dim position As Long
Dim search As String
Dim processor As String
Dim html As String
Const url = "https://partsurfer.hpe.com/Search.aspx?SearchText="
Dim xmlhttp As Object
Set xmlhttp = CreateObject("MSXML2.serverXMLHTTP")
xmlhttp.Open "POST", url & serial_number, False
xmlhttp.Send
html = xmlhttp.responseText
' find the tag that idenifies the processor in the html
search = "ctl00_BodyContentPlaceHolder_gridCOMBOM_ctl34_lblpartdesc1"">"
position = InStr(1, html, search) + Len(search)
processor = Split(Mid(html, position), "<")(0)
get_processor = processor
End Function
The code below uses Excel's built-in "web query" feature to pull the as you have described. This code brings in the value from the 33rd row of the table that comes back from the web request. However, I'm skeptical that the CPU data will always be on the 33rd row and I don't see a way based on results from the webserver to infer what the right line is. Hopefully, this gets you headed in the right direction.
Sub get_computer_data()
' be sure to set these two constants and a variable
Dim SheetName as String
SheetName = "computers" ' name of the sheet that has the computer data
Dim serialNumCol as Byte
serialNumCol = 5 ' the number of the column that has the serial number
Dim r As Long: r = 2 ' the row the computer data starts on
Dim url as String
url = "https://partsurfer.hpe.com/Search.aspx?SearchText="
Dim s As Worksheet ' a reference
Dim query As Worksheet ' a variable to refer to the sheet created by the web query
Dim cell As Range ' a range object used to find data in the query result
Set s = ThisWorkbook.Worksheets(SheetName) 'process a specific sheet
' process all rows of contiguous data
Do Until s.Cells(r, 1).Value = ""
'perform a web query for the current serial number
Set query = CreateWebQuery(url & s.Cells(r, serialNumCol).Value, xlAllTables)
' find the data on the result page
Set cell = query.Cells.Find("Part Description", , , xlWhole)
If cell Is Nothing Then
s.Cells(r, serialNumCol + 2).Value = "No Data"
Else
s.Cells(r, serialNumCol + 2).Value = cell.Offset(33)
End If
r = r + 1
Loop
End Sub
Function CreateWebQuery(url As String, Optional WebSelectionType As XlWebSelectionType = xlEntirePage, Optional SaveQuery As Boolean, Optional PlainText As Boolean = True, Optional SheetName As String = "webQuery") As Worksheet
'*********************************************************************************'
' Builds a web-query object to retrieve information from a web server and
' returns a reference to a worksheet containing the data
'
' Parameters:
'
'
' URL
' The webpage to get. Should start with "http"
'
' WebSelectionType (xlEntirePage or xlAllTables)
' what part of the page should be brought back to Excel.
'
' SaveQuery (True or False)
' Indicates if the query object remains in the workbook after running
'
' PlainText (True or False)
' Indicates if the query results should be plain or include formatting
'
' SheetName
' Indicates the name of the sheet to create or use
'
'*********************************************************************************'
Dim outsheet As Worksheet
Dim s As Worksheet
Set s = ActiveSheet
On Error Resume Next
Set outsheet = ThisWorkbook.Worksheets(SheetName)
If Err.Number = 0 Then
outsheet.Cells.Clear
Else
Set outsheet = ThisWorkbook.Worksheets.Add
outsheet.Name = SheetName
End If
On Error GoTo 0
s.Activate
With outsheet.QueryTables.Add(Connection:="URL;" & url, Destination:=outsheet.Range("a1"))
.Name = "WebQuery"
.RefreshStyle = xlOverwriteCells
.WebSelectionType = WebSelectionType
.PreserveFormatting = PlainText
.BackgroundQuery = False
.Refresh
If Not SaveQuery Then .Delete
End With
Set CreateWebQuery = outsheet
End Function

Sort Excel worksheets based on name, which is a date

So I've got this Excel workbook that has some macro's. Users are presented with a button to either create a worksheet with the current date as name, or enter a date manually and that worksheet will be created.
Now the issue: The worksheet has two sheet ('Initial' and 'Version') that must be first and last. However, all worksheets created in between should be sorted on date everytime a new sheet is created. And I mean sorted on date, the sheets are 'DD-MM-YY' so e.g. I could have names like '1-11-21', '2-11-21', '11-11-21' and '21-11-21' in the same workbook and it should be sorted ascending.
Any suggestions? A normal sort just messes things up I found (1-11-21 and 11-11-21, followed by '2-11-21' and '21-11-21'....
Thanks,
Jasper
Sorting sheets of a workbook is rather easy, there a numerous examples out there, looking more or less like this:
Sub SortSheets(Optional wb As Workbook = Nothing)
If wb Is Nothing Then Set wb = ActiveWorkbook ' (or maybe ThisWorkbook)
Application.ScreenUpdating = False
Dim i As Long, j As Long
For i = 1 To wb.Worksheets.Count - 1
For j = i + 1 To wb.Worksheets.Count
' ==> The following line needs to be replaced!
If wb.Worksheets(j).Name < wb.Worksheets(i).Name Then
wb.Worksheets(j).Move before:=wb.Worksheets(i)
End If
Next j
Next i
' Application.ScreenUpdating = True
End Sub
The only logic you need to change now is the If-statement. Instead of comparing the names of the sheets, you need to find a custom logic that compares the names of the two sheets.
Your logic is basically: If the name is Initial, sort it to the top, if it is Version, sort it to the end and for all the others, sort them by the date the name is representing.
I created a small function that calculates a number from the name. The Initial sheets gets 0, the Version gets a arbitrary high number, a worksheet with a date in the name gets the date value (a date is basically a double value in VBA) by converting the name into the date. If the name cannot be converted to a date, the value will be so that the sheet will be sorted to the end (but before the version sheet).
Function getSortNumber(ws As Worksheet) As Double
Const MaxNumber = 100000
If ws.Name = "Initial" Then
' Sort Initial to the beginning
getSortNumber = 0
ElseIf ws.Name = "Version" Then
' Sort Version to the end
getSortNumber = MaxNumber + ws.Parent.Sheets.Count
Else
' Create real date fom name
Dim d As Date, tokens() As String
tokens = Split(ws.Name, "-")
On Error Resume Next
d = DateSerial(Val(tokens(2)), Val(tokens(1)), Val(tokens(0)))
On Error GoTo 0
If d = 0 Then
' Failed to convert to date, sort to end
getSortNumber = MaxNumber + ws.Index
Else
' Sort according to the date value
getSortNumber = CDbl(d)
End If
End If
End Function
You can adapt the function easily if your needs changed (eg date format, or you can have extra text with the date, or you want to sort the version sheet to the beginning, or you have additional sheets with different names...). The sort function itself will not change at all, only the comparison logic.
Now all you have to do is change the line in the sort routine:
If wb.Worksheets(j).Name < wb.Worksheets(i).Name Then
to
If getSortNumber(wb.Worksheets(j)) < getSortNumber(wb.Worksheets(i)) Then
The general approach of converting the sheet names (that, hopefully, look like dates) to actual date serial numbers, and sorting those has been answered. But there is a a bit more to it than other answers show.
If your sheet names are user entered, you should handle a bit of variability
No need to reinvent Date Conversion, use whats already in Excel/VBA. But you need to define what year a 2 digit number represents, specifically which century it's in.
Note: How DateSerial interprets 2 digit dates is a bit complex. Refer to the docs for details
Decide what you want to do with sheets whose names cannot be converted to valid dates. Options include
Clean them up. eg
remove excess white space
allow for suffixes (times?)
alternate delimiters
other date forms (eg 1 Oct 2020)
etc
Aborting
Delete them
Move them to a defined location
Move them to another workbook
Prompt user for a new valid name
Generate a new valid name in the code
etc
Once the date serial numbers are created, you sort that data. Many options exist
Use the Dynamic Array function SORT, if you have it
If you don't, there are many Array Sort algorithms and implementations available for VBA
Examples 1 2
Use a data structure that supports Sorting. Example System.Collections.ArrayList 1
Dump the data onto a sheet and use Excel Sort
Once you have the sorted data, move the sheets into place. Note: another answer provide a nested For loop. This executes in order n^2 (n = number of sheets) May not matter for a smallish number of sheets, but will get much slower as the number of sheets increases. But it's easily avoided, see the code below.
Suggested methodoligy, including comments on what to change to suit your needs. Run this after the user has inserted a new sheet.
Sub SortSheets()
Dim ws As Worksheet
Dim wb As Workbook
Dim idx As Long
Dim SheetNames As Variant
Set wb = ThisWorkbook ' or specify the book you want
' Validate book contents
On Error Resume Next
Set ws = wb.Worksheets("Initial")
On Error GoTo 0
If ws Is Nothing Then
' Initial Doesn't exist. What now?
Exit Sub
End If
If ws.Index <> 1 Then
' Move it to first
ws.Move Before:=wb.Worksheets(1)
End If
On Error Resume Next
Set ws = wb.Worksheets("Version")
On Error GoTo 0
If ws Is Nothing Then
' Version Doesn't exist. What now?
Exit Sub
End If
If ws.Index <> wb.Worksheets.Count Then
' Move it to last
ws.Move After:=wb.Worksheets(wb.Worksheets.Count)
End If
' For each sheet between first and last,
' Convert Name to a dateSerial
' Handle any invalidly named sheets
ReDim SheetNames(2 To wb.Worksheets.Count - 1, 1 To 2)
For idx = 2 To wb.Worksheets.Count - 1
Set ws = wb.Worksheets(idx)
On Error Resume Next
' convert sheet name to date
SheetNames(idx, 1) = getDate(ws.Name)
On Error GoTo 0
If IsEmpty(SheetNames(idx, 1)) Then
' Invalid Sheet Name format. What Now?
' eg move it to the end (before Version)
SheetNames(idx, 1) = 3000000
' change to handle as you require, eg Delete it, Prompt user for a new name, etc
End If
SheetNames(idx, 2) = ws.Name
Next
' Sort on date using Dynamic Array Function SORT
SheetNames = Application.Sort(SheetNames)
' If SORT is not available, there are many Array Sort algorithms and implementations available
' Move sheets into position
' SheetNames is a 2D array of the DateSerial numbers and actual sheet names, sorted in the order we want them in the book
' Loop through the array lowest to highest,
' Get a reference to the sheet by name
' Move it to its required position (if it's not already there)
For idx = 1 To UBound(SheetNames, 1)
Set ws = wb.Worksheets(SheetNames(idx, 2))
If ws.Index <> idx + 1 Then
ws.Move After:=wb.Worksheets(idx)
End If
Next
End Sub
Function getDate(DateStr As String, Optional Delim As String = "-") As Long
' Cleanup sheet name
' Add or remove cleaning to suit your needs
' reduce multiple space sequences to single spaces
DateStr = Application.WorksheetFunction.Trim(DateStr)
' remove spaces aroung delimiter
DateStr = Replace$(DateStr, " " & Delim, Delim) '
DateStr = Replace$(DateStr, Delim & " ", Delim)
' replace any remaining spaces with delimiter (needed to make Val() work as desired)
DateStr = Replace$(DateStr, " ", Delim)
' Create real date from name
Dim d As Long, Segments() As String
Segments = Split(DateStr, Delim)
If UBound(Segments) < 2 Then
' not enough segments
d = 0
ElseIf UBound(Segments) > 2 Then
' too many segments. What Now?
' do nothing if it's acceptable to ignore anything after the date
Else
' Segment(0) is first part, assumed to be Day
' Segment(1) is second part, assumed to be Month
' Segment(2) is third part, assumed to be Year
' assume 2 digit dates are 2000's. Change to suit your needs
' Note: relying on DateSerial to convert 2 digit dates may give unexpected results
' as what you get depends on Excel version and local settings
If Len(Segments(2)) <= 2 Then Segments(2) = "20" & Format$(Segments(2), "00")
On Error Resume Next
d = CLng(DateSerial(CInt(Val(Segments(2))), CInt(Segments(1)), CInt(Segments(0))))
On Error GoTo 0
End If
If d = 0 Then
' Could not convert to date. Let calling routine decide what to do now
Err.Raise 1, "getDate", "Invalid Date string"
Else
' return date value
getDate = d
End If
End Function
Insert Date Worksheet
Note the following in two-digit year notation:
01/01/30 ... 01/01/1930
12/31/99 ... 12/31/1999
01/01/00 ... 01/01/2000
12/31/29 ... 12/31/2029
Some complications are present due to:
Sub Test1()
Debug.Print DateSerial(111, 22, 33) ' Result '11/02/112'
Debug.Print DateSerial(21, 2, 30) ' Result ' 03/02/2021
End Sub
The following will not sort any previously added worksheets. It will just insert the new worksheet in the right spot i.e. before the first worksheet with a greater date than the date supplied, or before the last worksheet (if no greater date).
Option Explicit
Sub InsertDateWorksheet()
' Needs 'RefWorksheet', 'InputDateText', 'GetTwoDigitYearDate' and 'IsLeapYear'.
Const ProcName As String = "InsertDateWorksheet"
Const First As String = "Initial"
Const Last As String = "Version"
Const Delimiter As String = "-"
Dim wb As Workbook: Set wb = ThisWorkbook
' First Worksheet
Dim fws As Worksheet: Set fws = RefWorksheet(wb, First, True)
If fws Is Nothing Then Exit Sub
If Not fws Is wb.Sheets(1) Then
fws.Move Before:=wb.Sheets(1)
End If
' Last Worksheet
Dim lws As Worksheet: Set lws = RefWorksheet(wb, Last, True)
If lws Is Nothing Then Exit Sub
Dim shCount As Long: shCount = wb.Sheets.Count
If Not lws Is wb.Sheets(shCount) Then
lws.Move After:=wb.Sheets(shCount)
End If
Dim NewDate As Date: NewDate = InputDateText(True)
If NewDate = 0 Then Exit Sub
Dim NewDateString As String: NewDateString = CStr(Day(NewDate)) _
& Delimiter & CStr(Month(NewDate)) & Delimiter _
& Right(CStr(Year(NewDate)), 2)
Dim nws As Worksheet: Set nws = RefWorksheet(wb, NewDateString)
If Not nws Is Nothing Then
MsgBox "The worksheet '" & NewDateString & "' already exists.", _
vbCritical, ProcName
Exit Sub
End If
Dim ws As Worksheet
Dim wsDate As Date
For Each ws In wb.Worksheets
Select Case ws.Name
Case First
Case Last
Exit For
Case Else
wsDate = GetTwoDigitYearDate(ws.Name, Delimiter)
If NewDate < wsDate Then
Exit For
End If
End Select
Next ws
Worksheets.Add(Before:=ws).Name = NewDateString
MsgBox "Worksheet '" & NewDateString & "' added.", vbInformation, ProcName
End Sub
Function RefWorksheet( _
ByVal wb As Workbook, _
ByVal WorksheetName As String, _
Optional ByVal DoWriteMessage As Boolean = False) _
As Worksheet
Const ProcName As String = "RefWorksheet"
On Error Resume Next
Set RefWorksheet = wb.Worksheets(WorksheetName)
On Error GoTo 0
If DoWriteMessage Then
If RefWorksheet Is Nothing Then
MsgBox "Worksheet '" & WorksheetName & "' not found.", _
vbCritical, ProcName
Exit Function
End If
End If
End Function
Function InputDateText( _
Optional ByVal DoWriteMessage As Boolean = False) _
As Date
' Needs 'GetTwoDigitYearDate' and 'IsLeapYear'.
Const ProcName As String = "InputDateText"
Const InputFormat As String = "d-m-yy"
Const nTitle As String = "Input Date Text"
Dim nPrompt As String
nPrompt = "Please enter a date in '" & InputFormat & "' format..."
Dim nDefault As String: nDefault = Format(Date, InputFormat)
Dim NewDateString As Variant: NewDateString = Application.InputBox( _
nPrompt, nTitle, nDefault, , , , , 2)
If NewDateString = False Then
MsgBox "You canceled.", vbExclamation, ProcName
Exit Function
End If
InputDateText = GetTwoDigitYearDate(NewDateString, "-")
If DoWriteMessage Then
If InputDateText = 0 Then
MsgBox "The string '" & NewDateString & "' is not valid.", _
vbCritical, ProcName
End If
End If
End Function
Function GetTwoDigitYearDate( _
ByVal DateString As String, _
Optional ByVal Delimiter As String = "-") _
As Date
' Needs 'IsLeapYear'.
On Error GoTo ClearError
Dim ArrDate() As String: ArrDate = Split(DateString, Delimiter)
Dim nYear As Long: nYear = CLng(ArrDate(2))
Select Case nYear
Case Is < 0, Is > 99
Exit Function
Case Else
nYear = IIf(nYear > 29, nYear + 1900, nYear + 2000)
End Select
Dim nMonth As Long: nMonth = CLng(ArrDate(1))
Select Case nMonth
Case Is < 1, Is > 12
Exit Function
End Select
Dim nDay As Long: nDay = CLng(ArrDate(0))
Select Case nDay
Case Is < 1, Is > 31
Exit Function
End Select
Select Case nMonth
Case 4, 6, 9, 11
If nDay = 31 Then Exit Function
Case 2
If nDay > 29 Then Exit Function
If nDay = 29 Then
If Not IsLeapYear(nYear) Then Exit Function
End If
End Select
GetTwoDigitYearDate = DateSerial(nYear, nMonth, nDay)
ProcExit:
Exit Function
ClearError:
Resume ProcExit
End Function
Function IsLeapYear( _
TestYear As Long) _
As Boolean
If TestYear Mod 4 = 0 Then
If TestYear Mod 100 = 0 Then
If TestYear Mod 400 = 0 Then
' Accounting for e.g. years 2000, 2400, 2800...8800, 9200, 9600.
IsLeapYear = True
'Else
' Accounting for e.g. years 2100, 2200, 2300...9700, 9800, 9900.
'isLeapYear = False
End If
Else
' Accounting for e.g. years 1904, 1908, 1912...1988, 1992, 1996.
IsLeapYear = True
End If
'Else
' Accounting for e.g. years 1901, 1902, 1903...1997, 1998, 1999.
'isLeapYear = False
End If
End Function

Subscript out of range error in Excel VBA when I add a new worksheet at the end of the list of worksheets only when the VB window is closed

I have built an Excel Workbook that is intended for evaluation of an organization as a whole and then evaluation of each of several sites for that organization. There is an initial assessment and then an on-site assessment for the organization, and for each facility. Depending on the organization, the number of facilities will vary. There is a first "Configuration" tab where the user of the workbook enters (or copies and pastes) the list of facilities and determines which facilities are to be included in the evaluation.
The second and third worksheets are the assessment for the organization as a whole, and the fourth and fifth worksheets are template assessment forms for the facilities.
Once the list of facilities is entered, the user clicks on a button labeled "Create Facility Tabs" that steps through the facility list and creates the needed worksheets for each facility.
If the list is fresh (starting from its initial form), then the template worksheets are renamed for the first facility and new worksheets are created for the remainder.
If there are already worksheets identified, the software checks each facility to see if its page already exists, and only creates additional worksheets for newly added facilities.
When additional worksheets are needed, the code first counts the number of additional worksheets that are needed (two for each facility), creates those worksheets, and then steps through them copying the template contents onto the forms and the change code for the worksheets into the worksheet's module.
The software works perfectly over and over again when I have the VBA window open. It does everything it is supposed to do. However, when I close the VBA window, the code creates all the worksheets, copies everything into the first worksheet, and then raises a Subscript Out of Range error. Any ideas what I am doing wrong?
Here is the code:
Public Sub CreateFacilities()
Dim row As Long
Dim facility_name As String
Dim facility_list As String
Dim facilities As Variant
Dim include As Boolean
Dim ws_init As Worksheet
Dim ws_fac As Worksheet
Dim ws_new_init As Worksheet
Dim ws_new_fac As Worksheet
Dim ws_config As Worksheet
Dim facility_count As Long
Dim tabs_to_create As Long
Dim fac_initial_range As Range
Dim fac_initial_address As String
Dim fac_onsite_range As Range
Dim fac_onsite_address As String
Dim message As String
Dim title As String
Dim answer As Variant
Dim code_line As String
Dim b_width As Long
Dim c_width As Long
Dim counter As Long
Dim init_sheet_number As Long
Dim fac_sheet_number As Long
Dim tab_count As Long
title = "Creating Facility Tabs"
message = "Before you execute this function you should" & vbCrLf & "add any study-specific questions to the" & vbCrLf
message = message & "Initial Assessment - Facility1 and" & vbCrLf & "On-Site Assessment - Facility1 tabs so" & vbCrLf
message = message & "they will be included on the created facility tabs" & vbCrLf & vbCrLf
message = message & "Do you wish to continue?"
answer = MsgBox(message, vbYesNo + vbQuestion, title)
If answer = vbNo Then
Exit Sub
End If
Set ws_config = ThisWorkbook.Sheets("Configuration")
Set ws_init = ThisWorkbook.Sheets(4)
Set ws_fac = ThisWorkbook.Sheets(5)
b_width = ws_init.Columns("B").ColumnWidth
c_width = ws_init.Columns("C").ColumnWidth
Set fac_initial_range = ws_init.Range("A1:C" & Trim(Str(FindLastRow(ws_init))))
Set fac_onsite_range = ws_fac.Range("A1:C" & Trim(Str(FindLastRow(ws_fac))))
fac_initial_address = fac_initial_range.address
fac_onsite_address = fac_onsite_range.address
code_line = ThisWorkbook.VBProject.VBComponents("Module1").CodeModule.Lines(1, 50) 'get code for each new worksheet
facility_list = "" 'get list of facilities
facility_count = 0
For row = 4 To 54
facility_name = ThisWorkbook.Sheets("Configuration").cells(row, 2).value
include = ThisWorkbook.Sheets("Configuration").cells(row, 4).value
If facility_name = "" Then 'reached the end of the list
Exit For
Else:
If include Then 'the Do Assessment column is marked TRUE
If Not WorksheetExists(facility_name) Then 'the tabs for this facility do not already exist
facility_list = facility_list & facility_name & ","
End If
End If
End If
Next row
facility_list = Left(facility_list, Len(facility_list) - 1) 'remove trailing comma
If facility_list = "" Then 'no new facilities were added to the list
MsgBox "There were no facilties specified for inclusion"
Exit Sub
End If
facilities = Split(facility_list, ",") 'there is a list of facilities to add
facility_count = UBound(facilities) + 1
If ActiveWorkbook.Sheets.Count = 5 Then 'no facility tabs have been added
If facility_count = 1 Then 'there is only one facility - no tabs need to be added
tabs_to_create = 0
facility_name = facilities(0)
ws_init.Name = CreateInitialTabName(facility_name)
ws_fac.Name = CreateOnSiteTabName(facility_name)
Else:
tabs_to_create = (facility_count - 1) * 2
ActiveWorkbook.Sheets.Add After:=ActiveWorkbook.Sheets(ActiveWorkbook.Sheets.Count), Count:=tabs_to_create
For counter = LBound(facilities) To UBound(facilities)
facility_name = facilities(counter)
If counter = 0 Then 'rename the first two facility worksheets that already exist
ws_init.Name = CreateInitialTabName(facility_name)
ws_fac.Name = CreateOnSiteTabName(facility_name)
Else: 'for the rest, add worksheets and copy template content and code
init_sheet_number = ((counter - 1) * 2) + 6
fac_sheet_number = init_sheet_number + 1
Set ws_new_init = ActiveWorkbook.Sheets(init_sheet_number) 'create initial assessment sheet for facility
ws_new_init.Columns("B").ColumnWidth = b_width
ws_new_init.Columns("C").ColumnWidth = c_width
ws_new_init.Name = CreateInitialTabName(facility_name)
fac_initial_range.Copy Destination:=ws_new_init.Range("A1")
ThisWorkbook.VBProject.VBComponents(ws_new_init.CodeName).CodeModule.AddFromString code_line
Set ws_new_fac = ActiveWorkbook.Sheets(fac_sheet_number) 'create on-site assessment sheet for facility
ws_new_fac.Columns("B").ColumnWidth = b_width
ws_new_fac.Columns("C").ColumnWidth = c_width
ws_new_fac.Name = CreateOnSiteTabName(facility_name)
fac_onsite_range.Copy Destination:=ws_new_fac.Range("A1")
ThisWorkbook.VBProject.VBComponents(ws_new_fac.CodeName).CodeModule.AddFromString code_line
End If
Next counter
End If
Else: 'there are more than 5 tabs in the workbook - some were already added
tab_count = ActiveWorkbook.Sheets.Count
tabs_to_create = facility_count * 2
ActiveWorkbook.Sheets.Add After:=ActiveWorkbook.Sheets(tab_count), Count:=tabs_to_create
For counter = LBound(facilities) To UBound(facilities)
facility_name = facilities(counter)
init_sheet_number = (counter * 2) + (tab_count + 1)
fac_sheet_number = init_sheet_number + 1
Set ws_new_init = ActiveWorkbook.Sheets(init_sheet_number)
Set ws_new_fac = ActiveWorkbook.Sheets(fac_sheet_number)
ws_new_init.Name = CreateInitialTabName(facility_name)
ws_new_fac.Name = CreateOnSiteTabName(facility_name)
ws_new_init.Columns("B").ColumnWidth = b_width
ws_new_fac.Columns("B").ColumnWidth = b_width
ws_new_init.Columns("C").ColumnWidth = c_width
ws_new_fac.Columns("C").ColumnWidth = c_width
fac_initial_range.Copy Destination:=ws_new_init.Range("A1")
fac_onsite_range.Copy Destination:=ws_new_fac.Range("A1")
ThisWorkbook.VBProject.VBComponents(ws_new_init.CodeName).CodeModule.AddFromString code_line
ThisWorkbook.VBProject.VBComponents(ws_new_fac.CodeName).CodeModule.AddFromString code_line
Next counter
End If
ws_config.Activate
MsgBox Str(facility_count) & " facilities added"
End Sub

How do I join the word "Sheet" and an integer to form sheet code name

How can I concatenate the word "Sheet" with a number (say, 2) to form a string that can be used as the code name of a sheet.
I've tried the following piece of code but it doesn't seem to work.
Sh = "Sheet" & 2
Range("A1") = Sh.index
If you want to refer the sheet just based on index you could try something like this as well ... hope it works for you
Sub trial()
i = 2
Sheets(i).Select
End Sub
I assume you want to check if a given â–ºstring argument (CodeNameString) refers to a valid Code(Name) in the VBA project. *)
If so, the following function returns the worksheet to be set to memory; otherwise the second argument IsAvailable passed by reference will change to False and can be used for error checks (c.f. ExampleCall below).
Function SheetByCodename(ByVal CodeNameString As String, ByRef IsAvailable As Boolean) As Object
'check for same CodeName in Sheets collection
Dim ws As Object
For Each ws In ThisWorkbook.Sheets
If ws.CodeName = CodeNameString Then ' check for string identity
Set SheetByCodename = ws ' set sheet object to memory
IsAvailable = True ' assign true to 2nd argument passed ByRef
Exit For
End If
Next
End Function
Example call
Sub ExampleCall()
dim cnt As Long: cnt = 2 ' << change example counter
Dim okay As Boolean ' << needed variable passed as 2nd function argument
With SheetByCodename("Sheet" & cnt, okay)
If okay Then
Debug.Print _
"a) Worksheet Name: " & .Name & vbNewLine & _
"b) Sheet's Code(Name) in Project: " & .CodeName
Else
Debug.Print "Given string refers to no valid Code(Name)."
'do other stuff to handle the wrong input
End If
End With
End Sub
*) Take note of #RonRosenfeld 's important remarks in comment:
"Codename is assigned when the worksheet is created. It can be changed in the properties window. In order to change it programmatically, you need to enable Trust Access to the VBA object model. Otherwise, it's a read-only property. "

Problems calling Function with Argument (Need to pass File name from one function to another) Excel VBA

I am trying to integrate 2 functions.
I have one sub function which works to loop through all files one by one.
once it has identified the file name.
It should call the function to run, on the opened file.
I can not seem to find a way to pass this on,
I did some reading on calling functions with arguments but when i try this i get a "compile error seperate list or )"
Can you please point me in the right direction?
I have posted the code below:
Option Explicit
Option Base 1
Public Const DATASHEET As String = "MDFDATA"
Public Const TABLECONVERSIONSHEET As String = "TABLECONVERSION"
Public Const OPTIONSSHEET As String = "OPTIONS"
Public Const FinalSheet As String = "Final Sheet"
Public lByte_Order As Long 'byte order
Public lData_Groups As Long 'number of data groups
Public lChannel_Groups As Long 'number of channel groups
Public lChannels As Long 'number of channels
Public lTable_offset As Long 'row offset for the conversion table sheet
Sub OpenFiles()
Dim MyFolder As String
Dim MyFile As String
Dim sFile_Name As String 'MDF file name
Dim lFile_Number As Long 'file number
MyFolder = "C:\Users\Documents\Test"
sFile_Name = Dir(MyFolder & "\*.dat")
Do While sFile_Name <> ""
lFile_Number = FreeFile
Open sFile_Name For Binary Access Read Shared As lFile_Number
Call PARSE_MDF
Loop
End Sub
'==================================================================================================
' PARSE_MDF
' Main function
' Returns True if successful
'==================================================================================================
Function PARSE_MDF() As Boolean
Dim sFile_Name As String 'MDF file name
Dim lFile_Number As Long 'file number
Dim lData_Groups_Counter As Long 'data groups counter
Dim lChannel_Groups_Counter As Long 'channel groups counter
Dim lChannels_Counter As Long 'channels counter
Dim lRecords As Long 'number of records in data block
Dim lRecord_Length As Long 'length of record in data block
Dim lData_Group_Address As Long 'data group address
Dim lData_Address As Long 'data address
Dim lChannel_Group_Address As Long 'Channel group address
Dim lChannel_Address As Long 'Channel address
Dim byCol As Byte 'column counter for output
Dim wsData_Sheet As Worksheet 'main worksheet
Dim wsTable_Conversion_Sheet As Worksheet
Dim rFirst_Signal As Range 'first signal in channel group
Dim rLast_Signal As Range 'last signal in channel group
Dim rSignals As Range 'range of signal names for a channel group
Application.EnableEvents = False
lTable_offset = 0
Set wsTable_Conversion_Sheet = Workbooks(ActiveWorkbook.Name).Worksheets(TABLECONVERSIONSHEET)
Set wsData_Sheet = Workbooks(ActiveWorkbook.Name).Worksheets(DATASHEET)
'file selected
If sFile_Name <> "False" Then
'clear old data
wsData_Sheet.Columns.Clear
wsTable_Conversion_Sheet.Columns.ClearContents
'set headers
wsData_Sheet.Cells(1, 1).Value = "Signal name"
wsData_Sheet.Cells(2, 1).Value = "Data type"
wsData_Sheet.Cells(3, 1).Value = "Lsb"
wsData_Sheet.Cells(4, 1).Value = "Offset"
wsData_Sheet.Cells(5, 1).Value = "Bit length"
wsData_Sheet.Cells(6, 1).Value = "Formula ID"
wsData_Sheet.Cells(7, 1).Value = "Formula"
wsData_Sheet.Cells(8, 1).Value = "First Bit position"
wsData_Sheet.Cells(9, 1).Value = "Table length"
wsData_Sheet.Cells(10, 1).Value = "Start Row"
'offset columns because of headers
byCol = 2
'get file number
lFile_Number = FreeFile
'open file
'check file integrity
If IDBLOCK(lFile_Number) Then
'check data exists
If HDBLOCK(lFile_Number, lData_Group_Address) Then
'main iteration for data groups
For lData_Groups_Counter = 1 To lData_Groups
'check channel group exists
If DGBLOCK(lFile_Number, lData_Group_Address, lChannel_Group_Address, lData_Address) Then
'channel group iteration
For lChannel_Groups_Counter = 1 To lChannel_Groups
'get channel group data
Call CGBLOCK(lFile_Number, lChannel_Group_Address, lChannel_Address, lRecord_Length, lRecords)
'set the first signal range in this channel group
Set rFirst_Signal = wsData_Sheet.Cells(1, byCol)
'channels iteration
For lChannels_Counter = 1 To lChannels
'get channel data for each channel
Call CNBLOCK(lFile_Number, lChannel_Address, wsData_Sheet, byCol)
'excel fudge
If byCol <> 255 Then
byCol = byCol + 1
End If
Next 'lChannels_Counter
'set the last signal range in this channel group
Set rLast_Signal = wsData_Sheet.Cells(1, byCol - 1)
'format divider columns
wsData_Sheet.Columns(byCol).ColumnWidth = 5
wsData_Sheet.Columns(byCol).Interior.ColorIndex = 0
wsData_Sheet.Columns(byCol).Interior.Pattern = xlLightUp
wsData_Sheet.Columns(byCol).Interior.PatternColorIndex = xlAutomatic
'excel fudge
If byCol <> 255 Then
'for space between channels
byCol = byCol + 1
End If
Next 'lChannel_Groups_Counter
'get range of signals to get data for
Set rSignals = wsData_Sheet.Range(rFirst_Signal, rLast_Signal)
'get signal data
'no channel data in this data group
Else
PARSE_MDF = False
End If
Next 'lData_Groups_Counter
'no data in MDF file
Else
PARSE_MDF = False
End If
'not a MDF file
Else
PARSE_MDF = False
End If
'close file
Close #lFile_Number
'tidy up sheet
wsData_Sheet.Rows.EntireRow.AutoFit
wsData_Sheet.Columns.EntireColumn.AutoFit
wsData_Sheet.Rows("2:15").EntireRow.Delete
wsData_Sheet.Columns("A:A").EntireColumn.Delete
wsData_Sheet.Cells.HorizontalAlignment = xlCenter
'function ends normally
PARSE_MDF = True
'no file was selected
Else
PARSE_MDF = False
End If
Application.EnableEvents = True
End Function
What you need to do is to pass the found filename as argument to the function. Now your function does not have an argument to pass it on so first of all create one such for ex.
Function PARSE_MDF(ByVal myFilePath as String) As Boolean
Then you need to change your loop to call the function correctly for ex.
Do While sFile_Name <> ""
....
myboolenvaluetohodthereturnvalue = PARSE_MDF(sFile_Name)
.... 'does your funtion need to return value and be tested?
sFile_Name = Dir() 'Call dir again without parameter to skip to next found file
Loop
Otherwise I haven't checked your code but this should get you started..

Resources