May 2023
Open Classes
Ruby allows anyone … yes you … to customize things to your own liking. Update existing methods, add new ones, etc.
One way to accomplish this is through this concept of Open Classes. In our own code, we can open any class that has already been defined (either from the Core Library or our own code), and just put stuff in.
class String
def disguise
self[1...-1] = "*" * (self.size - 2)
self
end
end
"john smith".disguise
=> "j********h"
"43829".disguise
=> "4***9"
This #disguise
method hides all characters but the first and the last.
This method is fun, but isn’t really the focus - the point is we can add methods to all classes, even those from the Ruby core library (class String
in this case).
#All?
If you’ve used Ruby, you’ve probably used #all?
.
#all?
is defined in the Enumberable
module, which is included in a few Ruby classes, and for Array
, we can use it like:
[2,3,4].all?(&:positive?)
=> true
[2,3,4].all? do |number|
number > 2
end
=> false
But what if we want to know whether most of the elements of an array satisfy a certain condition, not all.
Let’s say we wanted to check if all but one of the values of an array of numbers are positive. We could do something like:
arr = [2, 4, 1, -3, 9]
arr.count(&:positive?) >= (arr.size - 1)
=> true
This is okay, but if we need this behavior often, let’s create a method to handle it.
#all_but?
Ideally, we’ll be able to use this new method like:
arr = [2, 4, 1, -3, 9]
arr.all_but?(1) do |val|
val.positive?
end
=> true
or with shorthand notation:
arr = [2, 4, 1, -3, 9]
arr.all_but?(1, &:positive?)
=> true
Open the Class
First step is to open the Array
class.
class Array
def all_but?(number = 0)
puts self
puts number
end
end
[1,2,3,4].all_but?(2)
=> [1,2,3,4]
=> 2
[8,9,10].all_but?
=> [8,9,10]
=> 0 # default argument
Simple as that - now we have #all_but?
available for our arrays.
Iterate
Now that we see that we have access to self
inside our method, we iterate through it, to implement some sort of count.
class Array
def all_but?(number = 0)
count = 0
# self is implied (ie self.each)
each { |item| count += 1 if [..some logic..] }
count
end
end
Yield
We’ll use yield
to grab any code that was passed into the block.
arr = [2, 4, 1, -3, 9]
arr.all_but?(1) do |val|
val.positive?
end
In this example, we’ll access val.positive?
via the yield
method.
class Array
def all_but?(number = 0)
count = 0
each { |item| count += 1 if yield(item) }
count
end
end
We’re testing each
element of our array against the code we passed in the block.
Temporarily, we’re just returning the count, so with this implementation, we should expect to return 4
:
arr = [2, 4, 1, -3, 9]
arr.all_but?(1) do |val|
val.positive?
end
=> 4
Semi-final Touches
Instead of returning the count of elements that pass our test, let’s actually check the computed count against our target count.
This method is meant to check if all but a certain number of elements pass the test. So essentially, we’re excepting some number of elements from our test.
And we pass this exception count as an argument to #all_but
.
class Array
def all_but?(number = 0)
count = 0
each { |item| count += 1 if yield(item) }
count >= length - number
end
end
That’s it!*
Let’s run a couple of tests:
arr = [2, 4, -1, -3, 9]
arr.all_but?(1) do |val|
val.positive?
end
=> false
arr.all_but?(2) do |val|
val.positive?
end
=> true
* Now let’s consider an edge case.
Blockless
What if someone doesn’t pass in a block?
arr = [2, 4, 1, -3, 9]
arr.all_but?(1)
Currently, this will raise the exception LocalJumpError: no block given (yield)
. We can’t call yield
without a block.
Fortunately, Ruby gives us a handy block_given?
method.
class Array
def all_but?(number = 0)
count = 0
if !block_given?
# do something?
else
each { |item| count += 1 if yield(item) }
end
count >= length - number
end
end
Truthiness
The good news is that virtually all Ruby objects are truthy or falsey; meaning, they resolve to true
or false
.
All objects in Ruby except for nil
and false
evaluate to true true
. One way to test this is using the double bang.
!!3
=> true
!!"string"
=> true
!!""
=> true
!!true
=> true
!!nil
=> false
!!false
=> false
The double bang works by negating the object twice.
!(!3)
becomes !(false)
which becomes true.
So we don’t actually require code coming in from the block to test each item in the array, since each object can be treated as its own test.
For example, #all?
works in this same way:
[true, 2, "hello"].all?
=> true
[true, 2, "hello", nil].all?
=> false
Edge Case Continued
Let’s replace # do something
:
class Array
def all_but?(number = 0)
count = 0
if !block_given?
each { |item| count += 1 if item }
else
each { |item| count += 1 if yield(item) }
end
count >= length - number
end
end
Since each item of the array can evaluate to true
or false
, we can simply ask if [item]
.
In action:
[false, 2, "hello"].all_but(1)?
=> true
[true, 2, "hello", false, nil].all_but(1)?
=> false
Shorthand Notation
We’re basically done at this point, but just wanted to confirm that our method works with shorthand notation; that is; when the block is passed as the last argument of the method.
To accomplish this, we need to preface our block code with an &
.
arr = [2, 4, 1, -3, 9]
arr.all_but?(1, &:positive?)
=> true
The end
class Array
def all_but?(number = 0)
count = 0
if !block_given?
each { |item| count += 1 if item }
else
each { |item| count += 1 if yield(item) }
end
count >= length - number
end
end