Eric / Brooklyn

Random 5

Operator as Methods

Web Accounts

Honeypot Bots

Stripe and Tap

Git Save

Whitespace Problems

Ruby Exits

Appending in Javascript

Sendgrid Ban

Clean URLs

Integer Division

Multi-tab Websockets

Bad Content

JS Data Chutzpah

Responsive tables

Concerns

Cookies

Emoji Bits

Git

Ruby vs. Ruby

Extending Devise

Rails UJS

ENV Variables

See All

A New Enumerable Method

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