Ruby’s String#delete and Boolean Algebra
How can learning about the String#delete method in Ruby help us understand Boolean Algebra?
It starts with a misuse of gsub
I cannot tell you how many times have I written code like
'abc-de'.gsub('-','')
when I could have just written
'abc-de'.delete('-')
Fortunately, Stack Overflow was there to educate me. Not only is delete
more semantic and concise than gsub
for removing a single character, it gets even better with more complex deletions.
Delete with Multiple Characters
Imagine that you are working with a long string, maybe one containing the alphabet.
alphabet = 'abcdefghijklmnopqrstuvwxyz'
If you wanted to remove all of the vowels, starting with what we’ve seen from delete
so far, you could do something atrocious like:
alphabet.delete('a').delete('e').delete('i').delete('o').delete('u')
# => 'bcdfghjklmnpqrstvwxyz'
Fortunately, the developers of Ruby provide a much simpler approach for this use case. Instead of chaining all these method calls together, you can pass all of the characters at once, and delete
will remove any matches.
alphabet.delete('aeiou')
# => 'bcdfghjklmnpqrstvwxyz'
When executing alphabet.delete('aeiou')
, we are not deleting the string 'aeiou'
from alphabet
; that string does not exist as a substring of alphabet
. Instead, we are deleting any element from the set of letters {a,e,i,o,u}
.
Consequently, if you did want to delete a substring from alphabet
, e.g. ‘lmnop’
, I would recommend using slice!.
Delete with Multiple Parameters
A feature that makes delete
even cooler is how it handles multiple parameters. Working with the same alphabet
string, imagine you now want to remove all of the letters that satisfy both of the following criteria:
- Is a vowel
- Is in the first quartile of the alphabet (i.e. a-g)
delete
is perfect for this! While delete
treats a single argument string as a set of characters, when supplied with multiple argument strings, delete
will remove the intersection of those sets.
Remember an intersection (∩) is like AND, whereas a union (⋃) is like OR; more on this later
First, let’s take a look at delete
handling each of our criteria independently.
vowels = 'aeiou'
first_quartile = 'abcdefg'alphabet.delete(vowels)
# => 'bcdfghjklmnpqrstvwxyz'
alphabet.delete(first_quartile)
# => 'hijklmnopqrstuvwxyz'
Now let’s see what happens when we supply both strings as arguments to delete
.
alphabet.delete(vowels, first_quartile)
# => 'bcdfghijklmnopqrstuvwxyz'
The only letters deleted from alphabet
were ‘a’
and ‘e’
, because they were the only letters that appeared in all of the strings passed as parameters. That is equivalent to the intersection of the sets of characters represented by our two argument strings.
{a,e,i,o,u} ∩ {a,b,c,d,e,f,g} = {a,e}
Interestingly, the result of using delete
with both arguments is the same as the union of the results of using delete
with each of the arguments independently, once sorted.
{b,c,d,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z} = {b,c,d,f,g,h,j,k,l,m,n,p,q,r,s,t,v,w,x,y,z} ⋃ {h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z}
Expressed another way, deleting the intersection of vowels
and first_quartile
is equal to the union of deleting vowels
and deleting first_quartile
.
# Call split so we can use Array's intersection (&) and union (|) operatorsintersection = alphabet.delete(
(
vowels.split('') &
first_quartile.split('')
).join
)
# => "bcdfghijklmnopqrstuvwxyz"union = (
alphabet.delete(vowels).split('') |
alphabet.delete(first_quartile).split('')
).sort.join
# => "bcdfghijklmnopqrstuvwxyz"intersection == union
# => true
.sort
is necessary since union concatenates the two arrays
I think that code got really confusing really fast. But if we replace alphabet.delete
with !
, and remove the array methods for simplicity, we are left with:
!(vowels && first_quartile) == !vowels || !first_quartile
Simplified to the general case, that would look like
!(A && B) == !A || !B
That’s pretty neat! And it makes a lot of sense if we think about it from a Boolean Algebra perspective.
For more about delete
, check out the documentation.
Boolean Algebra
If you’ve been writing code for any time at all, you are probably familiar with the boolean — a data type that can have either a true
or false
value (sometimes represented as 1
or 0
, respectively). Boolean Algebra, then, is the branch of algebra dealing with booleans. It is fundamental to understanding the construction of logical conditions. The properties of Boolean Algebra — its operations, identities, and laws — are often reminiscent of those in arithmetic.
Boolean Algebra is the branch of algebra in which the values of the variables are the truth values ‘true’ and ‘false’, usually denoted 1 and 0 respectively
We are going to talk about a few boolean operations, focusing on the operators AND, OR, and NOT. Here is a glossary of symbols, each of which you might encounter depending on the source. The “Style” is not actual nomenclature, but rather my way of referring to the area of study where you are most likely to encounter each group of symbols.
+-------+-----+-----+-----+
| Style | AND | OR | NOT |
+-------+-----+-----+-----+
| Set | A∩B | A⋃B | Ā |
| Code | A&B | A|B | !A |
| Math | A·B | A+B | ~A |
| Logic | A∧B | A∨B | ¬A |
| | | | A' |
+-------+-----+-----+-----+
Note: In code the logical AND (&) and OR (|) operators are often doubled (&&, ||) to differentiate from bitwise operators.
These three primary operations are analogous to Intersection (AND), Union (OR), and Complement (NOT).
Let’s see an example. If I say “my car is red (R) and fast (F)”, is that true? We can determine the veracity of the statement as a whole with a truth table, a table that lists all possible values for the inputs (R, F) and the resulting output (R & F).
+-------++-------+
| R | F || R & F |
+---+---++-------+
| 0 | 0 || 0 | // not red, not fast -> not red and fast
| 0 | 1 || 0 | // not red, fast -> not red and fast
| 1 | 0 || 0 | // red, not fast -> not red and fast
| 1 | 1 || 1 | // red, fast -> red and fast
+---+---++-------+
My car is only “red and fast” if it is both red and fast. So far, this just sounds like common sense. But something else to notice is that, if you look at my car and see it is blue, you know my car is not “red and fast” without even seeing how fast it is. In coding, this is called “short-circuiting,” meaning that if any part of an AND condition is false, the entire condition is false and the rest of the condition does not need to be evaluated.
+-------++-------+
| R | F || R & F |
+---+---++-------+
| 0 | x || 0 | // not red, unknown fast -> not red and fast
| x | 0 || 0 | // unknown red, not fast -> not red and fast
| 1 | 1 || 1 | // red, fast -> red and fast
+---+---++-------+
On the other hand, what if I said “my car is red or fast.”
+-------++-------+
| R | F || R | F |
+---+---++-------+
| 0 | 0 || 0 | // not red, not fast -> not red or fast
| 0 | 1 || 1 | // not red, fast -> red or fast
| 1 | 0 || 1 | // red, not fast -> red or fast
| 1 | 1 || 1 | // red, fast -> red or fast
+---+---++-------+
Now, as long as my car is red, it is “red or fast,” regardless of the speed. Again, we can short-circuit the evaluation of this statement, because if any part of an OR condition is true, the entire condition is true and the rest of the condition does not need to be evaluated.
+-------++-------+
| R | F || R | F |
+---+---++-------+
| 0 | 0 || 0 | // not red, not fast -> not red or fast
| x | 1 || 1 | // unknown red, fast -> red or fast
| 1 | x || 1 | // red, unknown fast -> red or fast
+---+---++-------+
More formally, these properties are codified in the Identity and Annihilator Laws.
Identity for ∧: 𝓍 ∧ 1 = 𝓍
Identity for ∨: 𝓍 ∨ 0 = 𝓍Annihilator for ∧: 𝓍 ∧ 0 = 0
Annihilator for ∨: 𝓍 ∨ 1 = 1
Knowing the four Identity and Annihilator laws will get you through most of the logic you’ll ever need for coding. However, with Ruby, which provides convenient logical complements such as if
and unless
, select
and reject
, or any?
and none?
, I think it helps to understand my favorite pair of laws of Boolean Algebra: De Morgan’s Laws.
De Morgan’s Laws
De Morgan’s Laws are “a pair of transformation rules…[that] allow the expression of conjunctions and disjunctions purely in terms of each other via negation,” as follows.
Negation of a disjunction: ¬(A ∨ B) = (¬A) ∧ (¬B)
Negation of a conjunction: ¬(A ∧ B) = (¬A) ∨ (¬B)
Here it is again “in English”:
the complement of the union of two sets is the same as the intersection of their complements; and the complement of the intersection of two sets is the same as the union of their complements.
To translate into Ruby:
- When you read ‘complement’ think
!
(NOT) - When you read ‘union’ think
||
(OR) - When you read ‘intersection’ think
&&
(AND)
Which gives us:
!(A || B) == !A && !B
!(A && B) == !A || !B
Just like we saw above!
Deleting Without Delete
To see De Morgan’s Laws in action, we can try a homegrown implementation of delete
using the Enumerable methods select
and reject
.
alphabet.split('').select {|c| !vowels.include?(c) || !first_quartile.include?(c) }.join
# retain the letters that are either NOT in vowels OR NOT in first_quartilealphabet.split('').reject {|c| vowels.include?(c) && first_quartile.include?(c)}.join
# discard the letters that are in BOTH vowels AND first_quartile
Since the String class does not include the Enumerable module, we first use split
to convert the alphabet to an array of characters; join
is used to reassemble the remaining characters back into a string.
With select
, we want to keep any letter that is either NOT in vowels
or NOT in first_quartile
; whereas with reject
, we want to discard any letter that IS in both of the strings. Because select
and reject
are complements of each other, their conditions should be logical inverses of each other as well. This can easily be demonstrated with a trivial exercise.
arr = [1,2,3,4,5]
arr.reject {|a| a.odd?} == arr.select {|a| !a.odd?}
# => true
Going back to our alphabet example, we should be able to demonstrate
# !(reject_condition) == select_condition!(vowels.include?(c) && first_quartile.include?(c)) == !vowels.include?(c) || !first_quartile.include?(c)
If we replace vowels.include?(c)
with A and first_quartile.include?(c)
with B, we get
!(A && B) == !A || !B
Et voila! Is this starting to look familiar yet?
Keeping De Morgan’s Laws in mind allows you to switch freely between select
and reject
, or other logical complements. You may find this helps you convert your logical expressions into something more intuitive. Is it easier for you to wrap your head around one code snippet over the other? (For me, reject
makes a lot more sense in this case.)
More Truth Tables and Karnaugh Maps
As with the “fast and red car” example, we can use a truth table for our alphabet
expression. In this case, to be consistent with what we’ll see later, we are using slightly different notation; !A || !B
becomes A' + B'
and !(A && B)
becomes '(A·B)
or simply '(AB)
.
+-------++---------+-------+----+
| A | B || A' + B' | '(AB) | AB |
+---+---++---------+-------+----+
| 0 | 0 || 1 | 1 | 0 |
| 0 | 1 || 1 | 1 | 0 |
| 1 | 0 || 1 | 1 | 0 |
| 1 | 1 || 0 | 0 | 1 |
+---+---++---------+-------+----+
The truth table confirms what we already demonstrated with boolean algebra — that the expressions A' + B'
and '(AB)
are equivalent, and both are the inverse of AB
.
Finally, we can use a tool called a Karnaugh Map, or K-map, to approach this problem from a slightly different angle. A K-map is useful for mapping a set of requirements to the desired logical outcomes, to derive a logical expression satisfying the requirements. Furthermore, if you already have a logical expression, K-maps can sometimes help you find a simpler expression that yields the same results. It is akin to a truth table, but laid out in a sort of grid.
+---------------+
| | A |
+ +---+---+
| | 0 | 1 | <- Input values for A
+---------------+
| B | 0 | | | <- Results (to be determined)
| | 1 | | |
+---+---+---+---+
^
Input values for B
So once again, let’s say that we want to keep the letters in the string alphabet
that are either not in vowels
(A) or not in first_quartile
(B). The K-map is filled out by evaluating our various inputs and filling in the corresponding outputs.
- If A is false (0) and B is false (0), the letter is not in
vowels
and not infirst_quartile
, and therefore should be kept in our string (1).
+---------------+
| | A |
+ +---+---+
| | 0 | 1 | <- Input values for A
+---------------+
| B | 0 | 1 | | <- Results
| | 1 | | |
+---+---+---+---+
^
Input values for B
- If A is true (1) and B is false (0), the letter is in
vowels
but not infirst_quartile
, and should be kept in our string (1).
+---------------+
| | A |
+ +---+---+
| | 0 | 1 | <- Input values for A
+---------------+
| B | 0 | 1 | 1 | <- Results
| | 1 | | |
+---+---+---+---+
^
Input values for B
- If A is false (0) and B is true (1), the letter is not in
vowels
but is infirst_quartile
, and should be kept in our string (1).
+---------------+
| | A |
+ +---+---+
| | 0 | 1 | <- Input values for A
+---------------+
| B | 0 | 1 | 1 | <- Results
| | 1 | 1 | |
+---+---+---+---+
^
Input values for B
- Finally, if A is true (1) and B is true (1), the letter is in
vowels
and is also infirst_quartile
, and thus should not be kept in our string (0).
+---------------+
| | A |
+ +---+---+
| | 0 | 1 | <- Input values for A
+---------------+
| B | 0 | 1 | 1 | <- Results
| | 1 | 1 | 0 |
+---+---+---+---+
^
Input values for B
Now that the K-map has been filled out, the goal is to find the minimum number of maximum (largest) groupings that cover all true outcomes, as illustrated below (sometimes this is much simpler than others).
The groupings represent logical expressions.
- The red rectangle in the image above corresponds to A = 0, or A’
- The blue rectangle in the image corresponds to B = 0, or B’.
- Even though both rectangles cover the condition A=0, B=0, this duplication is preferable as it yields a simpler logical expression.
To combine both predicates, we use a union, yielding A’ + B’ (hence K=A'+B'
in the image); the complement is also listed, K’=AB
. Once again, this is consistent with what we saw before!
!A || !B == !(A && B)
Boolean algebra is just one of the many topics engineers at Def Method chat about in our free time. If you’re facing a technical challenge, we’d love to work with you. And we’re hiring!