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:
- 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.
- The English person lives in the red house.
- The Spaniard owns the dog.
- Coffee is drunk in the green house.
- The Ukrainian drinks tea.
- The green house is immediately to the right (your right) of the ivory house.
- The geranium grower owns snails.
- Roses are in front of the yellow house.
- Milk is drunk in the middle house.
- The Norwegian lives in the first house on the left.
- The person who grows marigolds lives in the house next to the person with the fox.
- Roses are grown at the house next to the house where the horse is kept.
- The person who grows lilies drinks orange juice.
- The Japanese person grows gardenias.
- 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:
These custom enums also include a special "Unset" value, which is an intentional piece of defensive programming that I wrote about here:
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."
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
'============================================================================