4

Is there a more efficient way to handle code execution based on multiple criteria than what I've written below? For three criteria you have potentially nine alternative results and it will scale exponentially with every new criteria added.

I've got code that has six separate criteria where you could either use one or all of them to achieve the wanted result. Using the below method of checking which criteria have been chosen forces the creation of 36 separate blocks of code and makes it a pain to add new ones.

I'm having a complete creative block with this particular project and cannot for the life of me figure out a more efficient way of doing it that will be easier to scale should additional criteria be warranted further down the line.

I would appreciate any help anyone could give. I can post the actual code but I'm more interested in a general solution so that I'm able to implement it in other projects in future as opposed to solving one specific problem.

It doesn't need to be "IsEmpty" and could be substituted with any Boolean or, for that matter, strings, ints or any other case result.

Select Case IsEmpty(x) & IsEmpty(y) & IsEmpty(z)

    Case Is = True & True & True

        'do stuff

    Case Is = False & True & True

        'do stuff

    Case Is = True & False & True

        'do stuff

    Case Is = True & True & False

        'do stuff

    Case is = False & False & True

        'do stuff

End Select

Thanks in advance!

Edit:

Since writing the above question I've continued to try and solve the problem I was having of exponentially increasing if statements. I came up with the below approach which works fairly well and thought I'd share in case anyone else was having a similar problem.

Instead of having an if statement for each potential outcome I created an array that gets fed with names corresponding to each parameter's function name. Then I call each of those functions every loop. That way, if I want to add new parameters I can just add another function.

If I had six parameters that would equate to 36 if statements to account for every potential search outcome. With this method I only need six short functions.

I'm sure there are millions of improvements I could make to the code to make it run faster but it works well for avoiding combinatorial explosion when dealing with multiple parameters.

    Public Sub SearchStuff()

    Dim book As Workbook
    Dim shResult As Worksheet
    Dim shSource As Worksheet

    Set book = ThisWorkbook
    Set shResult = book.Worksheets("Sheet1")
    Set shSource = book.Worksheets("Sheet2")

    shResult.EnableCalculation = False

    'Parameters avaiable to search with
    Dim param1 As Range
    Dim param2 As Range
    Dim param3 As Range
    Set param1 = shResult.Range("A1")
    Set param2 = shResult.Range("A2")
    Set param3 = shResult.Range("A3")       

    'Boolean expressions of whether or not the above parameters are being used
    Dim isUsedParam1 As Boolean
    Dim isUsedParam2 As Boolean
    Dim isUsedParam3 As Boolean
    isUsedParam1 = Not IsEmpty(param1)
    isUsedParam2 = Not IsEmpty(param2)
    isUsedParam3 = Not IsEmpty(param3)

    Dim lastSearchRow As Long
    lastSearchRow = shSource.Cells(Rows.Count, "A").End(xlUp).Row

    Dim rngSearch As Range
    Set rngSearch = shSource.Range("A2:A" & lastSearchRow)

    Dim lastRow As Long
    Dim rngOutput As Range
    Dim rngToCopy As Range
    Dim noSearchCriteriaProvided As Boolean

    Dim firstSectionToCopy As Range
    Dim secondSectionToCopy As Range
    Dim thirdSectionToCopy As Range

    Dim loopingCell As Range
    For Each loopingCell In rngSearch

        If noSearchCriteriaProvided = True Then

            MsgBox "No search criteria provided." & vbNewLine & vbNewLine & "Please select at least one criteria to search for and try again.", , "Whoopsie!"

            Exit Sub

        End If

        lastRow = shResult.Cells(Rows.Count, "B").End(xlUp).Row
        Set rngOutput = shResult.Range("B" & lastRow + 1)

        If CheckParams(isUsedDU, isUsedELR, isUsedNUM, isUsedFault, isUsedMil, loopingCell, shResult, noSearchCriteriaProvided) = True Then

            Set firstSectionToCopy = shSource.Range("A" & loopingCell.Row, "C" & loopingCell.Row)
            Set secondSectionToCopy = shSource.Range("E" & loopingCell.Row, "I" & loopingCell.Row)
            Set thirdSectionToCopy = shSource.Range("K" & loopingCell.Row, "M" & loopingCell.Row)
            Set rngToCopy = Union(firstSectionToCopy, secondSectionToCopy, thirdSectionToCopy)

            rngToCopy.Copy Destination:=rngOutput

        End If

    Next

    shResult.EnableCalculation = True

End Sub

Public Function CheckParams(isUsedParam1 As Boolean, isUsedParam2 As Boolean, isUsedParam3 As Boolean, loopingCell As Range, shResult As Worksheet, noSearchCriteriaProvided As Boolean) As Boolean

    Dim arraySize As Long
    arraySize = 0

    Dim myArray() As String
    Dim funcTitle As String
    Dim modTitle As String

    ReDim myArray(0)

    If isUsedParam1 = True Then

        arraySize = arraySize + 1
        ReDim Preserve myArray(arraySize - 1)

        myArray(arraySize - 1) = "CheckForParam1Match"

    End If

    If isUsedParam2 = True Then

        arraySize = arraySize + 1
        ReDim Preserve myArray(arraySize - 1)

        myArray(arraySize - 1) = "CheckForParam2Match"

    End If

    If isUsedParam3 = True Then

        arraySize = arraySize + 1
        ReDim Preserve myArray(arraySize - 1)

        myArray(arraySize - 1) = "CheckForParam3Match"

    End If


    'CHECKS IF ARRAY IS "EMPTY"
    If myArray(0) = vbNullString Then

        noSearchCriteriaProvided = True

        Exit Function

    End If

    For i = LBound(myArray) To UBound(myArray)

        funcTitle = myArray(i)
        modTitle = "Search."

        If Application.Run(modTitle & funcTitle, loopingCell, shResult) = False Then

            Exit Function

        End If

    Next

    CheckParams = True

End Function

Function CheckForParam1Match(loopingCell As Range, shResult As Worksheet) As Boolean

    Dim param1 As Range
    Set param1 = shResult.Range("A1")

    If loopingCell.Offset(0, 4).Value = param1.Value Then

        CheckForDUMatch = True

    End If

End Function

Function CheckForParam2Match(loopingCell As Range, shResult As Worksheet) As Boolean

    Dim param2 As Range
    Set param2 = shResult.Range("A2")

    If loopingCell.Offset(0, 5).Value = param2.Value Then

        CheckForELRMatch = True

    End If

End Function

Function CheckForParam3Match(loopingCell As Range, shResult As Worksheet) As Boolean

    Dim param3 As Range
    Set param3 = shResult.Range("A3")

    If loopingCell.Offset(0, 6).Value = param3.Value Then

        CheckForNUMMatch = True

    End If

End Function
Henrik
  • 43
  • 5
  • Welcome to the site! Check out the [how-to-ask page](https://stackoverflow.com/help/how-to-ask) for more about asking questions that will attract quality answers. You can [edit your question](https://stackoverflow.com/posts/49983922/edit) to include more information. There are lots of ways to deal with combinatorial explosion; [here's one I found on Google](https://stackoverflow.com/q/684000/2877364) that recommends the Visitor design pattern. I don't think I can answer your question in general - it will depend on the specifics. In other languages, a lookup table of functions might work. – cxw Apr 23 '18 at 14:52
  • Maybe this should be at: https://codereview.stackexchange.com/ – Alisson Bezerra Apr 23 '18 at 14:53
  • Are `do stuff` blocks similiar by it's nature? – AntiDrondert Apr 23 '18 at 14:57
  • @cxw Even just knowing the proper term for the problem I'm having may help me loads in finding a solution online. I'm going to research combinatorial explosion and how to solve it. Thanks for the help! – Henrik Apr 23 '18 at 14:57
  • @AntiDrondert In the project I'm working on the `do stuff` blocks do very similar things but essentially operate as a filter. For example; x decides the length, y decides thickness and z width. Based on which parameters have been chosen the code would then copy entries from a database over to a summary sheet. I'm essentially creating a filter which I'm sure there are easier ways of making but really I'm trying to solve the problem of combinatorial explosion which @cxw kindly informed me it was called. – Henrik Apr 23 '18 at 15:03
  • 3
    Look into how you can combine `Dictionary` with `CallByName` (or `Application.Run`) to implement some kind of a strategy pattern. – Mathieu Guindon Apr 23 '18 at 15:05

3 Answers3

6

Having 6 separate criteria, each of which can independently be either true or false, is like having a six bit binary number:

000000
000001
000010
000011
000100
000101
000110
000111
001000
...
etc.

Cook up some code to calculate an Integer variable (N) which would have values 0 if all criteria were false through 63 if all criteria were true.

Associated with each value would be a macro ( like Macro0, Macro1, etc). Then all you would need would be something like:

Application.Run "Macro" & N
Gary's Student
  • 95,722
  • 10
  • 59
  • 99
  • 1
    You could include Mathieu Guindon hint, to use this in combination with a dictionary and `CallByName` as an alternative to `Application.Run` – FloLie Apr 23 '18 at 15:08
  • Isn't this just another way to write a select case like OP already has? How does this help exactly? It seems to just obfuscate the reading of code more than improve having to write code for each case. – Automate This Apr 23 '18 at 15:13
  • @PortlandRunner You are correct about not saving any lines of code for the lines **within each case**; what you save is the `Select Case` structure itself.......................I agree with you on the obfuscation issue. – Gary's Student Apr 23 '18 at 15:19
  • @Gary'sStuden I'll look into this for sure. Whilst it doesn't solve the problem of writing individual code blocks for each outcome, it may be that case that this is unavoidable for what I'm trying to achieve. Creating a library function removes the need for select case and allows me to simply write the outcome for each integer value which does save a lot of typing. Thank you for the advice! – Henrik Apr 23 '18 at 15:33
  • @Henrik **This is not a perfect solution** .........only a general one............I offer it as "food for thought" – Gary's Student Apr 23 '18 at 15:41
  • @FloLie **super good idea!** I will update this later today.................using a `Dictionary` or `Collection` will support having several different binary values mappied into a single macro! – Gary's Student Apr 23 '18 at 15:53
5

Interesting that @GarysStudent has the same thought. I have a library routine I'd created for this type of situation:

Option Explicit

Sub test()
    Dim boolA As Boolean
    Dim boolB As Boolean
    Dim boolC As Boolean

    boolA = True
    boolB = False
    boolC = False

    Dim combined As Long
    combined = BooleanToBits(boolA, boolB, boolC)
    Debug.Print "combined flags = " & combined

    Debug.Print "should be  5 = "; BooleanToBits(True, False, True)
    Debug.Print "should be  7 = "; BooleanToBits(True, True, True)
    Debug.Print "should be  3 = "; BooleanToBits(False, True, True)
    Debug.Print "should be 22 = "; BooleanToBits(True, False, True, True, False)
End Sub

Function BooleanToBits(ParamArray flag()) As Long
    '--- based on the number of boolean flags passed as parameters, this
    '    function determines how many bits to use and converts each value
    '    left-to-right: flag1=highest bit...flagN=lowest bit (1's place)
    Dim numBits As Long
    Dim setBit As Long
    numBits = UBound(flag)

    Dim i As Long
    Dim result As Long
    For i = LBound(flag) To UBound(flag)
        setBit = 2 ^ numBits
        If flag(i) = True Then
            result = result + setBit
        Else
            '--- it's already zero, so leave it
        End If
        numBits = numBits - 1
    Next i
    BooleanToBits = result
End Function
PeterT
  • 8,232
  • 1
  • 17
  • 38
  • thanks for this. Having code that elaborates on Gary'sStudent's suggestion above will give me a base to build off of. Much appreciated! – Henrik Apr 23 '18 at 15:35
1

You seem to be having problems with the Select Case statement.

The expression IsEmpty(x) & IsEmpty(y) & IsEmpty(z) is the concatenation of the three results of IsEmpty. For example it will result in TrueTrueTrue. Do you mean the logical AND?

Likewise, the case labels as you write them, for example Case Is = True, True, True means "If the expression Is = to True, or is True or is True then execuete the following "do stuff". Hardle what you mean.

As what you do mean is unclear, I cannot give you a solution, except the advise to lookup the selectcase statement.

Paul Ogilvie
  • 25,048
  • 4
  • 23
  • 41
  • Thanks for pointing that out @paul. I've made an edit to show the code written correctly. It's `Case is = True & True & True` as opposed to `Case is = True, True, True` – Henrik Apr 23 '18 at 15:18