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
Xcode - Configure: Error: No Acceptable C Compiler Found in $Path
Why Are Symbols Not Frozen Strings
Resque Multiple Workers in Development Mode
Converting an Array of Keys and an Array of Values into a Hash in Ruby
How to Attach a Message to Rspec Check
Rails Scope Find with Current User
How to Validate the Date Such That It Is After Today in Rails
How to Use an Actionview::Helper in a Ruby Script, Outside of Rails
Framework for Non-Web Ruby Project
Ruby on Rails: Errors.Add_To_Base VS. Errors.Add
How to Structure a Layout Template in Haml
Multiple Robots.Txt for Subdomains in Rails
Iterate Every Month with Date Objects
No Database Connection in Rails Console