I found this question here.
And really curious to know the technical explanation of how something like 30.seconds.ago is implemented in Rails.
Method chaining? Numeric usage as per:
http://api.rubyonrails.org/classes/Numeric.html#method-i-seconds .
What else?
Here is the implementation of the seconds:
def seconds
ActiveSupport::Duration.new(self, [[:seconds, self]])
end
And, here is the implementation of the ago:
# Calculates a new Time or Date that is as far in the past
# as this Duration represents.
def ago(time = ::Time.current)
sum(-1, time)
end
And, here is the implementation of the sum method that's used inside the ago:
def sum(sign, time = ::Time.current) #:nodoc:
parts.inject(time) do |t,(type,number)|
if t.acts_like?(:time) || t.acts_like?(:date)
if type == :seconds
t.since(sign * number)
else
t.advance(type => sign * number)
end
else
raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
end
end
end
To understand it fully, you should follow the method calls and look for their implementations in the Rails source code like I showed you just now.
One easy way to find a method definition inside Rails code base is to use source_location in your Rails console:
> 30.method(:seconds).source_location
# => ["/Users/rislam/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.3/lib/active_support/core_ext/numeric/time.rb", 19]
> 30.seconds.method(:ago).source_location
# => ["/Users/rislam/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.3/lib/active_support/duration.rb", 108]
Related
Hello i have this blog module where my posts can have 3 states "no_status" "in_draft" and "published", the user can set the publish_date and publish_end_date for his posts
while this range between the dates is fulfilled, the status of the post must be "published", and when it is finished return to "in_draft"
def post
if self.no_status? || self.in_draft?
if self.publish_date >=Date.today && self.publish_end <= Date.today
self.update_attribute :status, 'published'
end
elsif self.published?
if self.publish_date.past? && self.publish_end.past?
self.update_attribute :status, 'in_draft'
end
end
end
What is the proper way to manage this, i have a big problem with my conditions.
In the 1st branch your conditionals are mixed up now, they should be opposite (that's why it doesn't work as expected - you check than publish_date is greater that current date, but this is wrong - it must be in the past to have the post published today). So if you simply "mirror" your conditional operators it should work - but there are cleaner ways of writing the same:
Date.today.between?(publish_date, publish_end)
# or
(publish_date..publish_end).cover?(Date.today)
In the 2nd branch checking that pulish_end date is n the past should be enough. Checking if publish_date is in the past too is redundant - if it is not you have bigger problems what just a wrong status :) - this kind of basic data integrity is better to be addressed by model validations.
Also, the nested ifs are absolutely unnecessary here, they just make the code harder to reason about.
To summarize, something like the following should do the job (I'm not discussing here how this method is being used and whether it should be written this way or not - just addressing the initial question)
def post
new_status =
if published? && publish_end.past?
'in_draft'
elsif Date.today.between?(publish_date, publish_end)
'published'
end
update_attribute :status, new_status
end
You can use Object#in?
def post
if no_status? || in_draft?
update(status: 'published') if Date.today.in?(publish_date..publish_end)
elsif published?
update(status: 'in_draft') if publish_date.past? && publish_end.past?
end
end
(BTW you don't need self in Ruby every time)
You can use Range#cover?. It basically takes a range and checks if the date is withing the start/end;
(10.days.ago..1.day.ago).cover?(3.days.ago)
# true
So, in your case;
(publish_date..publish_end).cover?(Date.today)
I have a function, which returns a list of ID's, in the Rails caching guide I can see that an expiration can be set on the cached results, but I have implemented my caching somewhat differently.
def provide_book_ids(search_param)
#returned_ids ||= begin
search = client.search(query: search_param, :reload => true)
search.fetch
search.options[:query] = search_str
search.fetch(true)
search.map(&:id)
end
end
What is the recomennded way to set a 10 minute cache expiry, when written as above?
def provide_book_ids(search_param)
#returned_ids = Rails.cache.fetch("zendesk_ids", expires_in: 10.minutes) do
search = client.search(query: search_param, :reload => true)
search.fetch
search.options[:query] = search_str
search.fetch(true)
search.map(&:id)
end
end
I am assuming this code is part of some request-response cycle and not something else (for example a long running worker or some class that is initialized once in your app. In such a case you wouldn't want to use #returned_ids directly but instead call provide_book_ids to get the value, but from I understand that's not your scenario so provided approach above should work.
I have to create a list of 24 months with the same day amongst them, properly handling the months that do not have day 29, 30 or 31.
What I currently do is:
def dates_list(first_month, assigned_day)
(0...24).map do |period|
begin
(first_month + period.months).change(day: assigned_day)
rescue ArgumentError
(first_month + period.months).end_of_month
end
end
end
I need to rescue from ArgumentError as some cases raise it:
Date.parse('10-Feb-2019').change(day: 30)
# => ArgumentError: invalid date
I am looking for a safe and elegant solution that might already exist in ruby or rails. Something like:
Date.parse('10-Feb-2019').safe_change(day: 30) # => 28-Feb-2019
So I can write:
def dates_list(first_month, assigned_day)
(0...24).map do |period|
(first_month + period.months).safe_change(day: assigned_day)
end
end
Does that exist or I would need to monkey patch Date?
Workarounds (like a method that already creates this list) are very welcome.
UPDATE
The discussion about what to do with negative and 0 days made me realize this function is trying to guess the user's intent. And it also hard codes how many months to generate, and to generate by month.
This got me thinking what is this method doing? It's generates a list of advancing months, of a fixed size, and modifying them in a fixed way, and guessing what the user wants. If your function description includes "and" you probably need multiple functions. We separate generating the list of dates from modifying the list. We replace the hard coded parts with parameters. And instead of guessing what the user wants, we let them tell us with a block.
def date_generator(from, by:, how_many:)
(0...how_many).map do |period|
date = from + period.send(by)
yield date
end
end
The user can be very explicit about what they want to change. No surprises for the user nor the reader.
p date_generator(Date.parse('2019-02-01'), by: :month, how_many: 24) { |month|
month.change(day: month.end_of_month.day)
}
We can take this a step further by turning it into an Enumerator. Then you can have as many as you like and do whatever you like with them using normal Enumerable methods..
INFINITY = 1.0/0.0
def date_iterator(from, by:)
Enumerator.new do |block|
(0..INFINITY).each do |period|
date = from + period.send(by)
block << date
end
end
end
p date_iterator(Date.parse('2019-02-01'), by: :month)
.take(24).map { |date|
date.change(day: date.end_of_month.day)
}
Now you can generate any list of dates, iterating by any field, of any length, with any changes. Rather than being hidden in a method, what's happening is very explicit to the reader. And if you have a special, common case you an wrap this in a method.
And the final step would be to make it a Date method.
class Date
INFINITY = 1.0/0.0
def iterator(by:)
Enumerator.new do |block|
(0..INFINITY).each do |period|
date = self + period.send(by)
block << date
end
end
end
end
Date.parse('2019-02-01')
.iterator(by: :month)
.take(24).map { |date|
date.change(day: date.end_of_month.day)
}
And if you have a special, common case, you can write a special case function for it, give it a descriptive name, and document its special behaviors.
def next_two_years_of_months(date, day:)
if day <= 0
raise ArgumentError, "The day must be positive"
end
date.iterator(by: :month)
.take(24)
.map { |next_date|
next_date.change(day: [day, next_date.end_of_month.day].min)
}
end
PREVIOUS ANSWER
My first refactoring would be to remove the redundant code.
require 'date'
def dates_list(first_month, assigned_day)
(0...24).map do |period|
next_month = first_month + period.months
begin
next_month.change(day: assigned_day)
rescue ArgumentError
next_month.end_of_month
end
end
end
At this point, imo, the function is fine. It's clear what's happening. But you can take it a step further.
def dates_list(first_month, assigned_day)
(0...24).map do |period|
next_month = first_month + period.months
day = [assigned_day, next_month.end_of_month.day].min
next_month.change(day: day)
end
end
I think that's marginally better. It makes the decision a little more explicit and doesn't paper over other possible argument errors.
If you find yourself doing this a lot, you could add it as a Date method.
class Date
def change_day(day)
change(day: [day, end_of_month.day].min)
end
end
I'm not so hot on either change_day nor safe_change. Neither really says "this will use the day or if it's out of bounds the last day of the month" and I'm not sure how to express that.
I have a User model in a ROR application that has multiple methods like this
#getClient() returns an object that knows how to find certain info for a date
#processHeaders() is a function that processes output and updates some values in the database
#refreshToken() is function that is called when an error occurs when requesting data from the object returned by getClient()
def transactions_on_date(date)
if blocked?
# do something
else
begin
output = getClient().transactions(date)
processHeaders(output)
return output
rescue UnauthorizedError => ex
refresh_token()
output = getClient().transactions(date)
process_fitbit_rate_headers(output)
return output
end
end
end
def events_on_date(date)
if blocked?
# do something
else
begin
output = getClient().events(date)
processHeaders(output)
return output
rescue UnauthorizedError => ex
refresh_token()
output = getClient().events(date)
processHeaders(output)
return output
end
end
end
I have several functions in my User class that look exactly the same. The only difference among these functions is the line output = getClient().something(date). Is there a way that I can make this code look cleaner so that I do not have a repetitive list of functions.
The answer is usually passing in a block and doing it functional style:
def handle_blocking(date)
if blocked?
# do something
else
begin
output = yield(date)
processHeaders(output)
output
rescue UnauthorizedError => ex
refresh_token
output = yield(date)
process_fitbit_rate_headers(output)
output
end
end
end
Then you call it this way:
handle_blocking(date) do |date|
getClient.something(date)
end
That allows a lot of customization. The yield call executes the block of code you've supplied and passes in the date argument to it.
The process of DRYing up your code often involves looking for patterns and boiling them down to useful methods like this. Using a functional approach can keep things clean.
Yes, you can use Object#send: getClient().send(:method_name, date).
BTW, getClient is not a proper Ruby method name. It should be get_client.
How about a combination of both answers:
class User
def method_missing sym, *args
m_name = sym.to_s
if m_name.end_with? '_on_date'
prop = m_name.split('_').first.to_sym
handle_blocking(args.first) { getClient().send(prop, args.first) }
else
super(sym, *args)
end
end
def respond_to? sym, private=false
m_name.end_with?('_on_date') || super(sym, private)
end
def handle_blocking date
# see other answer
end
end
Then you can call "transaction_on_date", "events_on_date", "foo_on_date" and it would work.
I'm trying to convert a bunch of numbers from imperial to metric on the front end of my site depending on if the user has set their measurement_units to 'metric' or 'imperial'
I can just do #myWeight*.45 to convert the number, but what I want to do is write a helper method like this
def is_imperial?
if User.measurement_units == 'metric'
*0.453592
elsif User.measurement_units == 'imperial'
*1
end
end
then be able to do this: #myWeight*.is_imperial?
I'm just not sure how I would assign the *value to the method is_imperial?
Thanks for the help!
EDIT:
#myWeight is a float calculated from adding several numbers.
I'm just trying to find an elegant way of converting any number that shows up on the site to metric if the user has metric as the value in the measurement_units field on the User model.
I assumed I would need to create a helper method in the application_helper.rb. Is that not correct?
I think you mean something like this:
class User
def imperial
f_multiplier = 0.0
f_multiplier = !!(self.measurement_units == 'metric') ? 0.453592 : 1
imperial = self.weight * f_multiplier
end
end
puts #myWeight.imperial
If you want the measurement_units method to be dynamic based on the user, then I think you need to make it an instance method.
Modify the is_imperial? method to return the right number:
def is_imperial?
if measurement_units == 'metric'
0.453592
elsif measurement_units == 'imperial'
1
end
end
Then you can call the method with something like this:
#myWeight.send(:*, is_imperial?)
If #myWeight represents a User object you might have to change it to this:
#myWeight.weight.send(:*, is_imperial?)
Methods that end with a ? in Ruby are expected to return true or false, so you should rename the method to be something like weight_conversion_factor.
Assuming #myWeight is Float value, you seem like you are looking for how to monkey patch. Check in rails console, #myWeight.class.name returns Float.
For monkey patching in Rails,
Create config/initializers/extensions directory. This is where you will store any future monkey patched methods.
Create a file called, floats.rb.
Add the following code.
class Float
def is_imperial?
if User.measurement_units == 'metric'
self*0.453592
elsif User.measurement_units == 'imperial'
self*1
end
end
end
Make sure to restart the Rails server to reinitialize.