Logic Puzzle Solution Part 1: The oNeighborhood Class Module

In part 1 of my Access solution for the five-house logic problem, I demonstrate the class module I used to model a neighborhood configuration.

Logic Puzzle Solution Part 1: The oNeighborhood Class Module

This post is a followup to the Five-House Logic Problem.  I described how I worked out the solution by hand in an earlier post (warning: contains spoilers).  This post is Part 1 of my Microsoft Access solution.

The Five-House Logic Problem

As a brief reminder–and for easy reference–here are the fifteen rules and two unanswered questions of the Five-House Logic Problem:

  1. There are five houses in a row, each of a different color and inhabited by people of different nationalities with different pets, drinks, and flowers.
  2. The English person lives in the red house.
  3. The Spaniard owns the dog.
  4. Coffee is drunk in the green house.
  5. The Ukrainian drinks tea.
  6. The green house is immediately to the right (your right) of the ivory house.
  7. The geranium grower owns snails.
  8. Roses are in front of the yellow house.
  9. Milk is drunk in the middle house.
  10. The Norwegian lives in the first house on the left.
  11. The person who grows marigolds lives in the house next to the person with the fox.
  12. Roses are grown at the house next to the house where the horse is kept.
  13. The person who grows lilies drinks orange juice.
  14. The Japanese person grows gardenias.
  15. The Norwegian lives next to the blue house.

Who drinks water? And who owns the zebra?

Manual vs. Automatic

It took me nearly two hours to work out the solution by hand (though others, such as Maria Barnes, solved it in a quarter of that time).

I like to tell myself that I took that long because my mind kept wandering into how I would solve this problem in VBA.  I imagined a brute-force approach.  I would write a class module to model a neighborhood configuration.  The class module would have a function to test whether the current configuration satisfied the riddle.  I would then systematically create and test every possible permutation of neighborhood configurations until I found one that met the rules of the riddle.

In this article, I'm going to describe the class module I created for this purpose.

Reader Benefits

I know it's unlikely that a client or your boss would ever ask you to write an Access application to solve a decades-old riddle.  

However, these sorts of fun side projects (yes, I find this type of thing fun) often force you to explore new techniques that you can then apply in your normal work.  Throughout this series of articles, I will be highlighting some techniques that you may not be familiar with, but might find useful in other contexts.

My hope is that you will gain some value by reading and seeing these techniques in my solution.

The SatisfiesRiddle Function

The heart of the  oNeighborhood class is the SatisfiesRiddle function.

This function uses a series of guard clauses to verify whether this particular neighborhood configuration satisfies the 15 rules of the riddle. As soon as any one of the guard closes fails, the function exits and returns false. If all guard clauses pass it means the neighborhood meets all of the puzzle logic criteria. In this case, the function returns True.

This approach leads to code that is highly readable as the logic matches up one for one with the 15 puzzle rules.

Why guard clauses?

One disadvantage with guard clauses in this particular situation is that the logic is backwards.

Instead of checking to see whether the neighborhood configuration matches each rule, we instead check each rule to see if it fails.  This makes the logic slightly harder to follow, but there are enough advantages to outweigh this one disadvantage.

Advantages of the Guard Clause Approach
  • No nested If statements
  • Function exits immediately as soon as the first rule is violated
  • Each rule can be enforced with a single line of code
' ----------------------------------------------------------------
' Procedure : SatisfiesRiddle
' Date      : 5/25/2023
' Author    : Mike Wolfe
' Source    : https://nolongerset.com/oneighborhood/
' Purpose   : Returns True if this neighborhood configuration
'               satisfies the rules of the riddle
' Notes     - uses a series of guard clauses to check for each condition
'           - the entire neighborhood must be populated before calling this function
' ----------------------------------------------------------------
Public Function SatisfiesRiddle() As Boolean
    

    'The English person lives in the red house.
    If HouseByOwner(on_English).Color <> hc_Red Then Exit Function
    
    'The Spaniard owns the dog.
    If HouseByOwner(on_Spanish).Pet <> p_Dog Then Exit Function
    
    'Coffee is drunk in the green house.
    If HouseByDrink(d_Coffee).Color <> hc_Green Then Exit Function
    
    'The Ukrainian drinks tea.
    If HouseByOwner(on_Ukrainian).Drink <> d_Tea Then Exit Function
    
    'The green house is immediately to the right (your right) of the ivory house.
    If (HouseByColor(hc_Green).Position - HouseByColor(hc_Ivory).Position) <> 1 Then Exit Function
    
    'The geranium grower owns snails.
    If HouseByFlower(f_Geraniums).Pet <> p_Snails Then Exit Function
    
    'Roses are in front of the yellow house.
    If HouseByFlower(f_Roses).Color <> hc_Yellow Then Exit Function
    
    'Milk is drunk in the middle house.
    If this.House3.Drink <> d_Milk Then Exit Function

    'The Norwegian lives in the first house on the left.
    If this.House1.Owner <> on_Norwegian Then Exit Function
    
    'The person who grows marigolds lives in the house next to the person with the fox.
    If Abs(CInt(HouseByFlower(f_Marigolds).Position) - CInt(HouseByPet(p_Fox).Position)) <> 1 Then Exit Function
    
    'Roses are grown at the house next to the house where the horse is kept.
    If Abs(CInt(HouseByFlower(f_Roses).Position) - CInt(HouseByPet(p_Horse).Position)) <> 1 Then Exit Function
    
    'The person who grows lilies drinks orange juice.
    If HouseByFlower(f_Lilies).Drink <> d_OJ Then Exit Function
    
    'The Japanese person grows gardenias.
    If HouseByOwner(on_Japanese).Flower <> f_Gardenias Then Exit Function
    
    'The Norwegian lives next to the blue house.
    If Abs(CInt(HouseByOwner(on_Norwegian).Position) - CInt(HouseByColor(hc_Blue).Position)) <> 1 Then Exit Function
    
    'All rules are satisfied; return True
    SatisfiesRiddle = True
End Function

HouseByXxx Functions

The HouseByXxx functions are the key to making the SatisfiesRiddle function readable.

For example, the second rule–"The English person lives in the red house."–can be verified using the following simple guard clause:

If HouseByOwner(on_English).Color <> hc_Red Then Exit Function

Remember, these are guard clauses.  That means they "guard" the function from invalid data.  Thus, instead of checking to make sure the English person lives in the red house, we are checking to see if the English person DOES NOT live in the red house.  If they do not live in the red house, we know this particular neighborhood configuration is invalid and we immediately exit the function, returning False.

The HouseByXxx functions are straightforward and very similar to each other, so I'm only showing the HouseByOwner function here (see full class module code below for the other HouseByXxx functions).

Private Function HouseByOwner(Owner As on__OwnerNationality) As typHouse
    Dim i As Byte
    For i = 1 To 5
        If this.House(i).Owner = Owner Then
            HouseByOwner = this.House(i)
            Exit Function
        End If
    Next i
End Function

SetXxx Subroutines

The SetXxx subroutines are used to configure neighborhoods one item at a time.  

While we could have exposed the House array as a public property of the class, adding a layer of indirection provides us with the flexibility to more easily refactor our class in the future without breaking backward compatibility.

As with the HouseByXxx functions, the SetXxx Subs are very similar to each other, so I'm only going to show a single example routine:

Public Sub SetOwner(Position As Byte, Owner As on__OwnerNationality)
    this.House(Position).Owner = Owner
End Sub

Custom Enums

As you've likely inferred by now, I use custom enums to represent the owner nationalities, pet types, house colors, drinks, and flower types.

I use an unusual enum naming convention, which I cover here:

Enum Type Naming Convention
The combination of “IntelliSense overload” and “global identifier case changes” convinced me I needed a different approach.

These custom enums also include a special "Unset" value, which is an intentional piece of defensive programming that  I wrote about here:

The “Unset” Enum Item
This simple technique is a foolproof way to avoid the sort of logic bug that can live undetected in your codebase for years.

Here's a sample of the enum I wrote for pet types:

Public Enum p__Pet
    p_Unset
    p_Dog
    p_Snails
    p_Fox
    p_Horse
    p_Zebra
End Enum

Enum Descriptions

Unfortunately (or maybe fortunately), VBA does not include the sort of reflection that allows us to get at the identifier we used to define an enum item the way that VB.NET does.  The workaround is simple, if a bit verbose and tedious to write.

However, these functions highlight one of my favorite uses of the colon (:) as a statement separator character: to create compact Select...Case statements when appropriate.  For example:

Private Function PetText(Pet As p__Pet) As String
    Select Case Pet
    Case p_Unset: PetText = "{PET NOT SET}"
    Case p_Dog: PetText = "a dog"
    Case p_Snails: PetText = "snails"
    Case p_Fox: PetText = "a fox"
    Case p_Horse: PetText = "a horse"
    Case p_Zebra: PetText = "a zebra"
    Case Else: PetText = "{INVALID PET: " & Pet & "}"
    End Select
End Function

Notice also the Case Else statement, where I handle any unexpected p__Pet enum values.  Should our code ever hit the Case Else line?  No, it should not.  However, a good programmer is one who looks both ways before crossing a one-way street.

Neighborhood Description

Finally, I wanted a convenient and human-friendly way to output the current neighborhood configuration.  That's the job of the NeighborhoodDescription property:

' ----------------------------------------------------------------
' Procedure : NeighborhoodDescription
' Date      : 5/25/2023
' Author    : Mike Wolfe
' Source    : https://nolongerset.com/oneighborhood/
' Purpose   : Returns the full neighborhood layout
' ----------------------------------------------------------------
Public Property Get NeighborhoodDescription() As String
    Dim i As Byte, s As String
    For i = 1 To 5
        Dim House As typHouse
        House = this.House(i)
        s = s & "House " & i & ": "
        s = s & "The " & OwnerText(House.Owner) & " lives in "
        s = s & "the " & ColorText(House.Color) & " house.  "
        s = s & "This person drinks " & DrinkText(House.Drink) & ", "
        s = s & "has " & PetText(House.Pet) & ", and "
        s = s & "grows " & FlowerText(House.Flower) & "."
        If i < 5 Then s = s & vbNewLine
    Next i
    NeighborhoodDescription = s
End Property

this.House Array

I first learned the this. technique from either Rubberduck VBA cofounder Mathieu Guindon or longtime Access MVP Ben Clothier.  I can't remember exactly where I first read about the technique, but Ben wrote a guest post on Mathieu's blog that includes the technique within the section titled, "Creating our first class module."

VBA Class Modules: gateway to SOLID code
Core contributor to the Rubberduck project, co-author of Microsoft Access in a Sharepoint World (2011), Professional Access 2013 Development (2013), and Effective SQL: 61 Specific Ways to Write Bet…
With this approach, we have only a single module-level variable, called This. This enables us to get a nicely filtered IntelliSense listing only the backing field, which can be now the same name as the public member and this is now much easier to enforce with compiler’s help.

Using this technique, I create a UDT which is simply an array of the five houses of the neighborhood.  As part of the class initialize event, I populate a Position property which I need to reference in my SatisfiesRiddle function.

Private Type typThisClass
    'Array of houses in the neighborhood:
    '   - House(1): Far left
    '   - House(2): Center left
    '   - House(3): Middle
    '   - House(4): Center right
    '   - House(5): Far right
    House(1 To 5) As typHouse
End Type
Private this As typThisClass

Private Sub Class_Initialize()
    Dim i As Byte
    For i = 1 To 5
        this.House(i).Position = i
    Next i
End Sub

The Full Code

Here's the full code for the oNeighborhood class module:

Option Compare Database
Option Explicit

'https://nolongerset.com/enum-type-naming-convention/
Public Enum on__OwnerNationality
    on_Unset        'https://nolongerset.com/unset-enum-item/
    on_English
    on_Spanish
    on_Ukrainian
    on_Norwegian
    on_Japanese
End Enum

Public Enum p__Pet
    p_Unset
    p_Dog
    p_Snails
    p_Fox
    p_Horse
    p_Zebra
End Enum

Public Enum hc__HouseColor
    hc_Unset
    hc_Red
    hc_Green
    hc_Ivory
    hc_Yellow
    hc_Blue
End Enum

Public Enum d__Drink
    d_Unset
    d_Coffee
    d_Tea
    d_Milk
    d_OJ
    d_Water
End Enum

Public Enum f__Flower
    f_Unset
    f_Geraniums
    f_Roses
    f_Marigolds
    f_Lilies
    f_Gardenias
End Enum

Private Type typHouse
    Position As Integer
    Owner As on__OwnerNationality
    Pet As p__Pet
    Color As hc__HouseColor
    Drink As d__Drink
    Flower As f__Flower
End Type

Private Type typThisClass
    'Array of houses in the neighborhood:
    '   - House(1): Far left
    '   - House(2): Center left
    '   - House(3): Middle
    '   - House(4): Center right
    '   - House(5): Far right
    House(1 To 5) As typHouse
End Type
Private this As typThisClass

Private Sub Class_Initialize()
    Dim i As Byte
    For i = 1 To 5
        this.House(i).Position = i
    Next i
End Sub


' ----------------------------------------------------------------
' Procedure : SatisfiesRiddle
' Date      : 5/25/2023
' Author    : Mike Wolfe
' Source    : https://nolongerset.com/oneighborhood/
' Purpose   : Returns True if this neighborhood configuration
'               satisfies the rules of the riddle
' Notes     - uses a series of guard clauses to check for each condition
'           - the entire neighborhood must be populated before calling this function
' ----------------------------------------------------------------
Public Function SatisfiesRiddle() As Boolean
    

    'The English person lives in the red house.
    If HouseByOwner(on_English).Color <> hc_Red Then Exit Function
    
    'The Spaniard owns the dog.
    If HouseByOwner(on_Spanish).Pet <> p_Dog Then Exit Function
    
    'Coffee is drunk in the green house.
    If HouseByDrink(d_Coffee).Color <> hc_Green Then Exit Function
    
    'The Ukrainian drinks tea.
    If HouseByOwner(on_Ukrainian).Drink <> d_Tea Then Exit Function
    
    'The green house is immediately to the right (your right) of the ivory house.
    If (HouseByColor(hc_Green).Position - HouseByColor(hc_Ivory).Position) <> 1 Then Exit Function
    
    'The geranium grower owns snails.
    If HouseByFlower(f_Geraniums).Pet <> p_Snails Then Exit Function
    
    'Roses are in front of the yellow house.
    If HouseByFlower(f_Roses).Color <> hc_Yellow Then Exit Function
    
    'Milk is drunk in the middle house.
    If this.House(3).Drink <> d_Milk Then Exit Function

    'The Norwegian lives in the first house on the left.
    If this.House(1).Owner <> on_Norwegian Then Exit Function
    
    'The person who grows marigolds lives in the house next to the person with the fox.
    If Abs(CInt(HouseByFlower(f_Marigolds).Position) - CInt(HouseByPet(p_Fox).Position)) <> 1 Then Exit Function
    
    'Roses are grown at the house next to the house where the horse is kept.
    If Abs(CInt(HouseByFlower(f_Roses).Position) - CInt(HouseByPet(p_Horse).Position)) <> 1 Then Exit Function
    
    'The person who grows lilies drinks orange juice.
    If HouseByFlower(f_Lilies).Drink <> d_OJ Then Exit Function
    
    'The Japanese person grows gardenias.
    If HouseByOwner(on_Japanese).Flower <> f_Gardenias Then Exit Function
    
    'The Norwegian lives next to the blue house.
    If Abs(CInt(HouseByOwner(on_Norwegian).Position) - CInt(HouseByColor(hc_Blue).Position)) <> 1 Then Exit Function
    
    'All rules are satisfied; return True
    SatisfiesRiddle = True
End Function

'----------------------------------------------------------------------------
'   The "HouseByXxx" functions serve as lookups that return the house structure
'       that corresponds with the passed owner/pet/color/drink/flower
Private Function HouseByOwner(Owner As on__OwnerNationality) As typHouse
    Dim i As Byte
    For i = 1 To 5
        If this.House(i).Owner = Owner Then
            HouseByOwner = this.House(i)
            Exit Function
        End If
    Next i
End Function

Private Function HouseByPet(Pet As p__Pet) As typHouse
    Dim i As Byte
    For i = 1 To 5
        If this.House(i).Pet = Pet Then
            HouseByPet = this.House(i)
            Exit Function
        End If
    Next i
End Function

Private Function HouseByColor(Color As hc__HouseColor) As typHouse
    Dim i As Byte
    For i = 1 To 5
        If this.House(i).Color = Color Then
            HouseByColor = this.House(i)
            Exit Function
        End If
    Next i
End Function

Private Function HouseByDrink(Drink As d__Drink) As typHouse
    Dim i As Byte
    For i = 1 To 5
        If this.House(i).Drink = Drink Then
            HouseByDrink = this.House(i)
            Exit Function
        End If
    Next i
End Function

Private Function HouseByFlower(Flower As f__Flower) As typHouse
    Dim i As Byte
    For i = 1 To 5
        If this.House(i).Flower = Flower Then
            HouseByFlower = this.House(i)
            Exit Function
        End If
    Next i
End Function
'============================================================================



'----------------------------------------------------------------------------
'   The "SetXxx" functions are used to populate the neighborhood
'
Public Sub SetOwner(Position As Byte, Owner As on__OwnerNationality)
    this.House(Position).Owner = Owner
End Sub

Public Sub SetPet(Position As Byte, Pet As p__Pet)
    this.House(Position).Pet = Pet
End Sub

Public Sub SetColor(Position As Byte, Color As hc__HouseColor)
    this.House(Position).Color = Color
End Sub
    
Public Sub SetDrink(Position As Byte, Drink As d__Drink)
    this.House(Position).Drink = Drink
End Sub
    
Public Sub SetFlower(Position As Byte, Flower As f__Flower)
    this.House(Position).Flower = Flower
End Sub
'============================================================================
    
    
' ----------------------------------------------------------------
' Procedure : NeighborhoodDescription
' Date      : 5/25/2023
' Author    : Mike Wolfe
' Source    : https://nolongerset.com/oneighborhood/
' Purpose   : Returns the full neighborhood layout
' ----------------------------------------------------------------
Public Property Get NeighborhoodDescription() As String
    Dim i As Byte, s As String
    For i = 1 To 5
        Dim House As typHouse
        House = this.House(i)
        s = s & "House " & i & ": "
        s = s & "The " & OwnerText(House.Owner) & " lives in "
        s = s & "the " & ColorText(House.Color) & " house.  "
        s = s & "This person drinks " & DrinkText(House.Drink) & ", "
        s = s & "has " & PetText(House.Pet) & ", and "
        s = s & "grows " & FlowerText(House.Flower) & "."
        If i < 5 Then s = s & vbNewLine
    Next i
    NeighborhoodDescription = s
End Property


'----------------------------------------------------------------------------
'   The "XxxText" functions return friendly text versions of passed enum values
'
Private Function OwnerText(Owner As on__OwnerNationality) As String
    Select Case Owner
    Case on_Unset: OwnerText = "{OWNER NOT SET}"
    Case on_English: OwnerText = "English person"
    Case on_Spanish: OwnerText = "Spaniard"
    Case on_Ukrainian: OwnerText = "Ukrainian"
    Case on_Norwegian: OwnerText = "Norwegian"
    Case on_Japanese: OwnerText = "Japanese person"
    Case Else: OwnerText = "{INVALID OWNER: " & Owner & "}"
    End Select
End Function

Private Function ColorText(Color As hc__HouseColor) As String
    Select Case Color
    Case hc_Unset: ColorText = "{COLOR NOT SET}"
    Case hc_Red: ColorText = "red"
    Case hc_Green: ColorText = "green"
    Case hc_Ivory: ColorText = "ivory"
    Case hc_Yellow: ColorText = "yellow"
    Case hc_Blue: ColorText = "blue"
    Case Else: ColorText = "{INVALID COLOR: " & Color & "}"
    End Select
End Function

Private Function DrinkText(Drink As d__Drink) As String
    Select Case Drink
    Case d_Unset: DrinkText = "{DRINK NOT SET}"
    Case d_Coffee: DrinkText = "coffee"
    Case d_Tea: DrinkText = "tea"
    Case d_Milk: DrinkText = "milk"
    Case d_OJ: DrinkText = "orange juice"
    Case d_Water: DrinkText = "water"
    Case Else: DrinkText = "{INVALID DRINK: " & Drink & "}"
    End Select
End Function

Private Function PetText(Pet As p__Pet) As String
    Select Case Pet
    Case p_Unset: PetText = "{PET NOT SET}"
    Case p_Dog: PetText = "a dog"
    Case p_Snails: PetText = "snails"
    Case p_Fox: PetText = "a fox"
    Case p_Horse: PetText = "a horse"
    Case p_Zebra: PetText = "a zebra"
    Case Else: PetText = "{INVALID PET: " & Pet & "}"
    End Select
End Function

Private Function FlowerText(Flower As f__Flower) As String
    Select Case Flower
    Case f_Unset: FlowerText = "{FLOWER NOT SET}"
    Case f_Geraniums: FlowerText = "geraniums"
    Case f_Roses: FlowerText = "roses"
    Case f_Marigolds: FlowerText = "marigolds"
    Case f_Lilies: FlowerText = "lilies"
    Case f_Gardenias: FlowerText = "gardenias"
    Case Else: FlowerText = "{INVALID FLOWER: " & Flower & "}"
    End Select
End Function
'============================================================================

All original code samples by Mike Wolfe are licensed under CC BY 4.0