Ruby Detect Method

Ruby Detect method

Detect returns the first item in the list for which the block returns TRUE. Your first example:

>> [1,2,3,4,5,6,7].detect { |x| x.between?(3,4) }
=> 3

Returns 3 because that is the first item in the list that returns TRUE for the expression x.between?(3,4).

detect stops iterating after the condition returns true for the first time. select will iterate until the end of the input list is reached and returns all of the items where the block returned true.

Understanding `detect` method

According to the documentation this method returns the first element in the enumerable object that the block returns true.

Therefore, the first number in that range that is both divisible by 2 and 3 is 6 and thus it is returned. If this were not the case and no number was divisible by both 2 and 3, then the method will return nil.

It's a way to "detect" the first object that makes the block true.

Ruby's .where vs. detect

In this example

User.find(1)        # or
User.find_by(id: 1)

will be the fastest solutions. Because both queries tell the database to return exactly one record with a matching id. As soon as the database finds a matching record, it doesn't look further but returns that one record immediately.

Whereas

User.where(id: 1)

would return an array of objects matching the condition. That means: After a matching record was found the database would continue looking for other records to match the query and therefore always scan the whole database table. In this case – since id is very likely a column with unique values – it would return an array with only one instance.

In opposite to

User.all.detect { |u| u.id == 1 }

that would load all users from the database. This will result in loading thousands of users into memory, building ActiveRecord instances, iterating over that array and then throwing away all records that do not match the condition. This will be very slow compared to just loading matching records from the database.

Database management systems are optimized to run selection queries and you can improve their ability to do so by designing a useful schema and adding appropriate indexes. Every record loaded from the database will need to be translated into an instance of ActiveRecord and will consume memory - both operations are not for free. Therefore the rule of thumb should be: Whenever possible run queries directly in the database instead of in Ruby.

How to detect the end of a method chain in Ruby

Since I can't think of any way to detect the last method call, I'm going to suggest a different approach. It is similar to what ActiveRecord does; a proxy class that builds the options and doesn't fetch the data until you call a method that operates on the data.

class Flickr
def initialize
@result = FlickrResult.new
end

def method_missing(method, *args, &block)
if @result.data.respond_to?(method)
@result.run(method, args, block)
else
@result.append(method, args[0])
return self
end
end

class FlickrResult
attr_reader :data

def initialize
@data = []
@keys = []
@options = {}
end

def append(key, options)
@keys << key
@options.merge!(options) if options
end

def run(method, args, block)
if !@did_run
fetch_data
end

@data.send(method, *args, &block)
end

def fetch_data
puts "Only runs once!"
@url = @keys.join(".") + "?" + @options.map {|k, v| "#{k}=#{v}" }.join("&")
# use @url and fetch data..
@data = ["foo", "bar"]

@did_run = true
end
end
end

@flickr = Flickr.new
@flickr.groups(:group_id => "123").pools.thing.users(:user_id => "456")

@flickr.each {|f| p f }
# => "Only runs once!"
# => "foo"
# => "bar"
p @flickr.map {|f| f.upcase }
# => ["FOO", "BAR"]

It only fetches the data when you each or map it or whatever (any array method).

Ruby: Call a method on returned object to detect if wrapper was used

Object inheritance

What you could do is to extend the Object which is the he default root of all Ruby objects with your method. Something like

class Object
def wrapper?
false
end
end

And then you return a different object when wrapping is happening. Something like:

class WrappedObject
def initialize(helper, value)
@helper = helper
@value = value
end

# if this is necessary to do the wrapping
def to_s
@helper.tag.span class: "super-text" do
@value
end
end

def wrapper?
true
end
end

And in your function:

def wrapper(text)
WrappedObject.new(h, text)
end

Of course it would be nicer to use Ruby refinements

Singleton Method

If you are using Strings then you could also define a singleton method on the returned value. Create the default method on the Object as previously described:

class Object
def wrapper?
false
end
end

Then change your wrapper method:

def wrapper(text)
return_value = h.tag.span class: "super-text" do
text
end

return_value.define_singleton_method(:wrapper?) { true }

return_value
end

You could also use Object#tap here, to make it more clever.

How can I output a calculated value using .detect in Ruby on Rails? (or alternative to .detect)

It's not entirely clear what you're asking for, but it sounds like you're trying to add up a delay until you reach a certain condition, and return the record that triggered the condition at the same time.

You might approach that using Enumerable#detect like you have, but by keeping a tally on the side:

def next_event_info
next_event = nil
delay = 0

events.detect do |event|
case (self.event_status(event))
when "no status"
true
else
delay += contact.event_delay(event)
false
end
end

[ next_event, delay ]
end

Update for if you want to add up all delays for all events, but also find the first event with the status of "no status":

def next_event_info
next_event = nil
delay = 0.0

events.each do |event|
case (self.event_status(event))
when "no status"
# Only assign to next_event if it has not been previously
# assigned in this method call.
next_event ||= event
end

# Tally up the delays for all events, converting to floating
# point to ensure they're not native DB number types.
delay += contact.event_delay(event).to_f
end

{
:event => next_event,
:delay => delay
}
end

This will give you a Hash in return that you can interrogate as info[:event] or info[:delay]. Keep in mind to not abuse this method, for example:

# Each of these makes a method call, which is somewhat expensive
next_event = next_event_info[:event]
delay_to_event = next_event_info[:delay]

This will make two calls to this method, both of which will iterate over all the records and do the calculations. If you need to use it this way, you might as well make a special purpose function for each operation, or cache the result in a variable and use that:

# Make the method call once, save the results
event_info = next_event_info

# Use these results as required
next_event = event_info[:event]
delay_to_event = event_info[:delay]


Related Topics



Leave a reply



Submit