5
\$\begingroup\$

I am taking a user input which consists of a from date and a to date. Those dates are compared against 4 cells in a row - a post date, pause date, resume date, and a close date. My goal with this code is to generate an array of rows with dates active within my date range.

Primary concern is if I followed the most effective logic for sorting and determining valid dates. Any comments on best practices would be appreciated.

Private Function DateRange() As Variant
Dim postcell As Range
Dim pausecell As Range
Dim unpausecell As Range
Dim closecell As Range
Dim arr_validRows() As Variant
Dim ws As Worksheet
Set ws = Sheets(1)
ReDim arr_validRows(0) As Variant
Dim z As Range
For Each z In ws.Range("D3:D" & ws.Cells(ws.Rows.Count, 2).End(xlUp).row)
 Set postcell = z
 Set pausecell = z.Offset(0, 1)
 Set unpausecell = z.Offset(0, 2)
 Set closecell = z.Offset(0, 3)
 If Not closecell.Value = "?" Then
 If CDate(postcell.Value) <= this.ToDate Then
 If Not pausecell.Value = "" Then
 If CDate(pausecell.Value) >= this.FromDate Then
 ReDim Preserve arr_validRows(UBound(arr_validRows) + 1) As Variant
 arr_validRows(UBound(arr_validRows)) = z.row
 ElseIf CDate(pausecell.Value) < this.FromDate And CDate(unpausecell.Value) >= this.FromDate Then
 ReDim Preserve arr_validRows(UBound(arr_validRows) + 1) As Variant
 arr_validRows(UBound(arr_validRows)) = z.row
 End If
 Else
 If CDate(closecell.Value) >= this.FromDate Then
 ReDim Preserve arr_validRows(UBound(arr_validRows) + 1) As Variant
 arr_validRows(UBound(arr_validRows)) = z.row
 End If
 End If
 End If
 End If
Next z
DateRange = arr_validRows
End Function

Below is the full class module that this function is contained in. The AddToReports()sub is where the DateRange() function is called.

Class: Report_Generator

Option Explicit
Private Type Reports
 RequisitionNumber As String
 FromDate As Date
 ToDate As Date
 JobTitle As String
 JobLocation As String
 JobCategory As String
 RecruiterName As String
 TSViews As Long
 TSApplicants As Long
End Type
Private this As Reports
Public Property Let RequisitionNumber(ByVal inputValue As String)
 this.RequisitionNumber = inputValue
End Property
Public Property Get RequisitionNumber() As String
 RequisitionNumber = this.RequisitionNumber
End Property
Public Property Let JobTitle(ByVal inputValue As String)
 this.JobTitle = inputValue
End Property
Public Property Get JobTitle() As String
 JobTitle = this.JobTitle
End Property
Public Property Let JobLocation(ByVal inputValue As String)
 this.JobLocation = inputValue
End Property
Public Property Get JobLocation() As String
 JobLocation = this.JobLocation
End Property
Public Property Let JobCategory(ByVal inputValue As String)
 this.JobCategory = inputValue
End Property
Public Property Get JobCategory() As String
 JobCategory = this.JobCategory
End Property
Public Property Let RecruiterName(ByVal inputValue As String)
 this.RecruiterName = inputValue
End Property
Public Property Get RecruiterName() As String
 RecruiterName = this.RecruiterName
End Property
Public Property Get TSViews() As Long
 TSViews = this.TSViews
End Property
Public Property Get TSApplicants() As Long
 TSApplicants = this.TSApplicants
End Property
Public Property Get FromDate() As String
 FromDate = this.FromDate
End Property
Public Property Let FromDate(ByVal inputValue As String)
 this.FromDate = inputValue
End Property
Public Property Get ToDate() As String
 ToDate = this.ToDate
End Property
Public Property Let ToDate(ByVal inputValue As String)
 this.ToDate = inputValue
End Property
Private Function DateRange() As Variant
 Dim postcell As Range
 Dim pausecell As Range
 Dim unpausecell As Range
 Dim closecell As Range
 Dim arr_validRows() As Variant
 Dim ws As Worksheet
 Set ws = Sheets(1)
 ReDim arr_validRows(0) As Variant
 Dim z As Range
 For Each z In ws.Range("D3:D" & ws.Cells(ws.Rows.Count, 2).End(xlUp).row)
 Set postcell = z
 Set pausecell = z.Offset(0, 1)
 Set unpausecell = z.Offset(0, 2)
 Set closecell = z.Offset(0, 3)
 If Not closecell.Value = "?" Then
 If CDate(postcell.Value) <= this.ToDate Then
 If Not pausecell.Value = "" Then
 If CDate(pausecell.Value) >= this.FromDate Then
 ReDim Preserve arr_validRows(UBound(arr_validRows) + 1) As Variant
 arr_validRows(UBound(arr_validRows)) = z.row
 ElseIf CDate(pausecell.Value) < this.FromDate And CDate(unpausecell.Value) >= this.FromDate Then
 ReDim Preserve arr_validRows(UBound(arr_validRows) + 1) As Variant
 arr_validRows(UBound(arr_validRows)) = z.row
 End If
 Else
 If CDate(closecell.Value) >= this.FromDate Then
 ReDim Preserve arr_validRows(UBound(arr_validRows) + 1) As Variant
 arr_validRows(UBound(arr_validRows)) = z.row
 End If
 End If
 End If
 End If
 Next z
 DateRange = arr_validRows
End Function
Sub AddToReport(ByVal sheetname As String)
 Dim ws As Worksheet
 Dim newrow As Long
 Set ws = Sheets(1)
 Dim exists As Boolean
 exists = False
 Dim i As Integer
 For i = 1 To Worksheets.Count
 If Worksheets(i).Name = sheetname Then
 exists = True
 End If
 Next i
 If Not exists Then
 Call CreateSheet(sheetname)
 With ThisWorkbook.Worksheets(sheetname)
 .Range("1:1").Value = ws.Range("2:2").Value
 End With
 End If
 Dim array_rows() As Variant
 array_rows = DateRange()
 Dim z As Variant
 Dim w As Integer
 For z = 1 To UBound(array_rows)
 If z <= UBound(array_rows) Then
 With ThisWorkbook.Worksheets(sheetname)
 newrow = .Cells(.Rows.Count, 2).End(xlUp).row + 1
 .Range(newrow & ":" & newrow).Value = ws.Range(array_rows(z) & ":" & array_rows(z)).Value
 End With
 Else
 Exit For
 End If
 Next z
End Sub
Sub TimeSeriesSummation()
'Still in progress
End Sub
Sub AdvancedFilters( _
 ByVal reqnum_on As Boolean, _
 ByVal jobcategory_on As Boolean, _
 ByVal recruiter_on As Boolean, _
 ByVal jobtitle_on As Boolean, _
 ByVal joblocation_on As Boolean, _
 ByVal sheetname As String)
 Dim ws As Worksheet
 Set ws = ThisWorkbook.Worksheets(sheetname)
 With ws.Range("A:O")
 ws.AutoFilterMode = False
 If reqnum_on Then
 'field 1
 .AutoFilter field:=1, Criteria1:="<>" & this.RequisitionNumber
 End If
 If jobcategory_on Then
 'field 13
 .AutoFilter field:=13, Criteria1:="<>" & this.JobCategory
 End If
 If recruiter_on Then
 'field 14
 .AutoFilter field:=14, Criteria1:="<>" & this.RecruiterName
 End If
 If jobtitle_on Then
 'field 2
 .AutoFilter field:=2, Criteria1:="<>" & this.JobTitle
 End If
 If joblocation_on Then
 'field 3
 .AutoFilter field:=3, Criteria1:="<>" & this.JobLocation
 End If
 End With
 If reqnum_on Or jobcategory_on Or recruiter_on Or jobtitle_on Or joblocation_on Then
 ws.Range("B2:B" & ws.Cells(ws.Rows.Count, 2).End(xlUp).row).SpecialCells(xlCellTypeVisible).EntireRow.Delete
 ws.AutoFilterMode = False
 End If
End Sub
Private Sub CreateSheet(ByVal sheetname As String)
 Dim ws As Worksheet
 Set ws = ThisWorkbook.Sheets.Add(After:= _
 ThisWorkbook.Sheets(ThisWorkbook.Sheets.Count))
 ws.Name = sheetname
End Sub
Sub Statistics(ByVal sheetname As String)
 With ThisWorkbook.Worksheets(sheetname)
 .Range("Q3").Value = "Descriptive Statistics"
 .Range("Q4").Value = "Mean"
 .Range("Q5").Value = "Median"
 .Range("Q6").Value = "Std. Dev."
 .Range("Q7").Value = "Variance"
 .Range("R3").Value = "Total Days Active"
 .Range("S3").Value = "Views"
 .Range("T3").Value = "Applications"
 .Range("U3").Value = "Views-To-Applications"
 .Range("V3").Value = "Applications per Day"
 .Range("R4").Value = "=AVERAGE(H2ドル:H$" & .Cells(.Rows.Count, 2).End(xlUp).row & ")"
 .Range("R5").Value = "=MEDIAN(H2ドル:H$" & .Cells(.Rows.Count, 2).End(xlUp).row & ")"
 .Range("R6").Value = "=STDEVP(H2ドル:H$" & .Cells(.Rows.Count, 2).End(xlUp).row & ")"
 .Range("R7").Value = "=VARP(H2ドル:H$" & .Cells(.Rows.Count, 2).End(xlUp).row & ")"
 Dim sourceRange As Range
 Dim fillRange As Range
 Set sourceRange = .Range("R4:R7")
 Set fillRange = .Range("R4:V7")
 Call sourceRange.AutoFill(fillRange)
 .Range("R4:R7").NumberFormat = "0.00"
 .Range("S4:S7").NumberFormat = "0.00"
 .Range("T4:T7").NumberFormat = "0.00"
 .Range("U4:U7").NumberFormat = "0.00%"
 .Range("V4:V7").NumberFormat = "0.00"
 End With
End Sub
Sub FormatColumns(ByVal sheetname As String)
 With ThisWorkbook.Worksheets(sheetname)
 .Range("H:H").NumberFormat = "0.00"
 .Range("I:I").NumberFormat = "0"
 .Range("J:J").NumberFormat = "0"
 .Range("K:K").NumberFormat = "0.00%"
 .Range("L:L").NumberFormat = "0.00"
 .Columns("Q:W").EntireColumn.AutoFit
 .Columns("A:N").EntireColumn.AutoFit
 .Columns("E:G").EntireColumn.Hidden = True
 End With
End Sub

And just to provide even further context, below is the sub that is called when the user form is submitted.

Private Sub GenerateReportButton_Click()
 Application.ScreenUpdating = False
 Call ReportGenerator
 Application.ScreenUpdating = True
 If TotalsButton.Value Then
 Unload Me
 End If
End Sub
Sub ReportGenerator()
 Dim reqnum_on As Boolean
 Dim jobtitle_on As Boolean
 Dim joblocation_on As Boolean
 Dim jobcategory_on As Boolean
 Dim recruiter_on As Boolean
 Dim sheetname As String
 Dim Reports As Report_Generator
 Set Reports = New Report_Generator
 With Reports
 .RequisitionNumber = ReqNumBox.Text
 .FromDate = CDate(FromBox.Text)
 .ToDate = CDate(ToBox.Text)
 .JobTitle = (JobTitleBox.Text)
 .JobLocation = JobLocationBox.Text
 .JobCategory = JobCategoryComboBox.Text
 .RecruiterName = RecruiterComboBox.Text
 End With
 reqnum_on = False
 jobtitle_on = False
 joblocation_on = False
 jobcategory_on = False
 recruiter_on = False
 sheetname = Left(Format(Reports.FromDate, "mmm d") & " to " & Format(Reports.ToDate, "mmm d") & " ", 31)
 If Not Reports.RequisitionNumber = "" Then
 reqnum_on = True
 sheetname = Left(sheetname & "RQ", 31)
 End If
 If Not Reports.JobTitle = "" Then
 jobtitle_on = True
 sheetname = Left(sheetname & "JT", 31)
 End If
 If Not Reports.JobLocation = "" Then
 joblocation_on = True
 sheetname = Left(sheetname & "JL", 31)
 End If
 If Not Reports.JobCategory = "" Then
 jobcategory_on = True
 sheetname = Left(sheetname & "JC", 31)
 End If
 If Not Reports.RecruiterName = "" Then
 recruiter_on = True
 sheetname = Left(sheetname & "RN", 31)
 End If
 Reports.AddToReport (sheetname)
 Reports.AdvancedFilters reqnum_on, jobcategory_on, recruiter_on, jobtitle_on, joblocation_on, sheetname
 Reports.Statistics (sheetname)
 Reports.FormatColumns (sheetname)
End Sub
asked Aug 1, 2016 at 15:18
\$\endgroup\$
0

1 Answer 1

5
\$\begingroup\$

DateRange() function

Array handling

You have a bug. The first element of arr_validRows will never be populated, because you give yourself a single element on this line...

ReDim arr_validRows(0) As Variant

...but when you add items to the array, you increase the bound before you add the new item:

ReDim Preserve arr_validRows(UBound(arr_validRows) + 1) As Variant
arr_validRows(UBound(arr_validRows)) = z.row 

This means that the calling function doesn't have an easy way to determine if there are any results other than the non-obvious method of checking to see if there are 2 items in the returned array. Since you are returning a Variant, I would suggest returning either vbEmpty or some other non-array value if there are no results - this makes checking the return value simply IsArray(foo).

Still on the topic of arrays, resizing arrays in a loop is horribly inefficient. Every time you use ReDim Preserve, the entire memory area of the array is copied. Using a Collection is roughly 10 times faster (benchmarked with 100,000 inserts). A Scripting.Dictionary is slightly faster than a Collection, and makes it much easier to return a Variant containing an array (it has a .Keys method).

Excel specific

Calculating offsets and requesting Range objects from Excel is also expensive. You already have your If conditions set up to "short circuit VBA style", but each time you go through the loop you collect all of the Range's before you know whether you'll need them or not. For example, if this test fails you don't need to retrieve any of the others:

If Not closecell.Value = "?" Then

Since your offsets are all fixed (and you have a reference to the worksheet), you can skip some overhead by using direct cell addresses. For example, z.Offset(0, 1) can be replaced with ws.Cells(z.Row, 1). The only Range that you use for anything other than its value is z.

So... you should probably be pulling the .Value's into variables instead of the Range's. For example, in this section of code it's possible to request pausecell.Value 3 times.

If Not pausecell.Value = "" Then
 If CDate(pausecell.Value) >= this.FromDate Then
 '...
 ElseIf CDate(pausecell.Value) < this.FromDate And CDate(unpausecell.Value) >= this.FromDate Then
 '...
 End If

Each one of those is basically a function call that is going to return the exact same result.

Miscellania

The variable postcell is always the same as z (and has a much better name). I'd just use it as the loop variable.

You aren't checking for invalid casts anywhere. One simple way to check this is with the IsDate function. It's generally a good idea to treat a Worksheet as user input - no telling what is going to be in a cell.

EDIT:

A couple of things that I noticed in the additional code that was posted for the class:

AddToReport method

You can exit your loop that checks for existing worksheet names early if you find a match:

For i = 1 To Worksheets.Count
 If Worksheets(i).Name = sheetname Then
 exists = True
 Exit For
 End If
Next I

Using .Range to access rows (and concatenating the index) is unnecessary - you can use .Rows and just provide your index directly:

.Range(newrow & ":" & newrow).Value = ws.Range(array_rows(z) & ":" & array_rows(z)).Value
'...can become...
.Rows(newrow).Value = ws.Rows(array_rows(z)).Value

This is a bit more efficient and much more readable.

The test If z <= UBound(array_rows) Then is unnecessary because your loop counter is already bound by that condition and the UBound can't change inside the loop. It can be omitted entirely.

With blocks should be outside of loops unless the object they are referring to can change. Remember, each With keyword is at least one dereference.

The value for newrow is repeatedly calculating the last row of the Worksheet. You only need to do this once - afterward you can simply increment it:

With ThisWorkbook.Worksheets(sheetname)
 newrow = .Cells(.Rows.Count, 2).End(xlUp).Row + 1
 For z = 1 To UBound(array_row) '1 based index due to bug in DateRange()
 .Rows(newrow).Value = ws.Rows(array_rows(z)).Value
 newrow = newrow + 1
 Next
End With

AdvancedFilters method

I would consider a method with 6 state flag variables to be a candidate for creating a new class to hold that state. It would be much cleaner (and more in line with SRP) to simply extract this functionality into a ReportFilter class that is responsible for Worksheet filtering. It could probably use a more descriptive name as well - if the only thing I knew about the method was its name, I'd be pretty surprised when it started deleting rows.

More Miscellania

I'm a bit up in the air about storing member variables in a user type - it seems like a bit of overkill. When they have the same names as properties and are assigned to a variable named this, I'm not in the air any more. When your member variables are accessed in the properties, it looks like a stack overflow at first glance because this implies (at least to me) an instance of the class:

Public Property Let RequisitionNumber(ByVal inputValue As String)
 this.RequisitionNumber = inputValue
End Property

A class can be thought of as a structure with additional functionality, but I wouldn't take that literally. I find it much more simple and more readable to just create independent backing variables to use:

Private mRequisitionNumber As String
Public Property Let RequisitionNumber(ByVal inputValue As String)
 mRequisitionNumber = inputValue
End Property

That saves the next person who sees the code from doing the mental gymnastics of trying to remember that this.RequisitionNumber and Me.RequisitionNumber are 2 entirely different things.

That said, if you like the UDT, I'd at very least name it something else (like maybe backingVariables or privateMembers). C# programmers will be grateful.

answered Aug 1, 2016 at 17:13
\$\endgroup\$
4
  • \$\begingroup\$ When you wrote the comment:For z = 1 To UBound(array_row) '1 based index due to bug in DateRange() is this a comment that z is indexed at 1 instead of 0 because array_row(0) is empty (the whole ReDim mess). Also what do you mean by "short circuit VBA style?" \$\endgroup\$ Commented Aug 1, 2016 at 19:26
  • 1
    \$\begingroup\$ @RollTideBrad - For the first question, yes. For the second question, VBA always evaluates every condition in an If statement. Most? other languages will stop evaluating as soon as they hit a condition that makes it impossible not to take one branch. I.e., if VBA short circuited you could safely do If IsDate(foo) And CDate(foo) > Now Then. Without short circuiting you can't, because it will still try to cast foo if it can't be cast to a Date -- it doesn't just branch when IsDate(foo) is false. \$\endgroup\$ Commented Aug 1, 2016 at 19:31
  • \$\begingroup\$ Okay, thank you. Both of those answers explain some of the early problems I had. The empty array_row(0) was a byproduct of multiple attempts of getting that section of AddToReport() to work. I played until it started working. \$\endgroup\$ Commented Aug 1, 2016 at 19:35
  • 1
    \$\begingroup\$ @RollTideBrad - Usually I've seen people add an "array initialized flag" that gets checked before expanding it. In this case another container would be the way I'd go. \$\endgroup\$ Commented Aug 1, 2016 at 19:38

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.