Are default arguments in ruby methods static? This matters when default arguments are meant to be a dynamic date, such as Date.today. Consider the following example in a rails application:
class User < ApplicationRecord
def eligible_to_vote?(date_of_interest = Date.today)
p date_of_interest
end
end
Will the default date_of_interest date always be the the same, static date from when I started the rails server?
Or: will it always dynamically grab "today's date" within the context of the date that the eligible_to_vote? method was called?
I know the following would for sure dynamically grab Date.today:
# Always dynamically sets `date` variable when no argument passed in
class User < ApplicationRecord
def eligible_to_vote?(date_of_interest = nil)
date = date_of_interest.present? ? date_of_interest : Date.today
p date_of_interest
end
end
What I'm mostly interested in is if default method arguments are dynamically generated or not. Whatever the answer is, it would be nice to have some official reference to answer this question as well. Thanks!
What I'm mostly interested in is if default method arguments are dynamically generated or not.
It would be trivially easy to test this:
def are_default_arguments_evaluated_every_time?(optional_parameter = puts('YES!')) end
are_default_arguments_evaluated_every_time?
# YES!
are_default_arguments_evaluated_every_time?
# YES!
If default arguments were evaluated at method definition time, this would print YES! only once, and before calling the method. If default arguments were evaluated only on the first call and then cached, this would print YES! only once, when the method first gets called.
Whatever the answer is, it would be nice to have some official reference to answer this question as well.
This is specified in section ยง13.3.3 Method invocation, step h), sub-step 7), sub-sub-step i) of the ISO Ruby Language Specification.
It is dynamic because Ruby is interpreted, not compiled language.
โ require 'date'
โ def test param = DateTime.now
puts param
end
โ 3.times { test; sleep(1) }
2018-12-14T18:10:08+01:00
2018-12-14T18:10:09+01:00
2018-12-14T18:10:10+01:00
it's not static, Date.today is evaluated every time.
Take for example:
def foo(some_var = boo)
p some_var
end
def boo
p "this is boo"
Date.today
end
When you run today
foo
# "this is boo"
# Fri, 14 Dec 2018
foo
# "this is boo"
# Fri, 14 Dec 2018
When you run tomorrow
foo
# "this is boo"
# Fri, 15 Dec 2018
Related
I'd like to use a Ruby Refinement to monkey patch an ActiveSupport method for outputting time in a specific format.
My ultimate goal is to have JSON.pretty_generate(active_record.as_json) print all timestamps in UTC, iso8601, 6 decimals. And I want to have all other timestamp printing behave normally.
This is what I have so far:
module ActiveSupportExtensions
refine ActiveSupport::TimeWithZone do
def to_s(_format = :default)
utc.iso8601(6)
end
end
end
class Export
using ActiveSupportExtensions
def export
puts JSON.pretty_generate(User.last.as_json(only: [:created_at]))
end
end
Export.new.export
Which outputs the following (not what I want).
{
"created_at": "2022-04-05 14:36:07 -0700"
}
What's interesting, is if I monkey patch this the regular way:
class ActiveSupport::TimeWithZone
def to_s
utc.iso8601(6)
end
end
puts JSON.pretty_generate(User.last.as_json(only: [:created_at]))
I get exactly what I want:
{
"created_at": "2022-04-05T21:36:07.878101Z"
}
The only issue is that this overrides the entire applications TimeWithZone class, which is not something I want to do for obvious reasons.
Thanks to Lam Phan comment, it's not possible via a refinement unfortunately.
However I was able to do it by override the default timestamp format.
# save previous default value
previous_default = ::Time::DATE_FORMATS[:default]
# set new default to be the utc timezone with iso8601 format
::Time::DATE_FORMATS[:default] = proc { |time| time.utc.iso8601(6) }
puts JSON.pretty_generate(User.last.as_json(only: [:created_at]))
# set the default back if we have one
if previous_default.blank?
::Time::DATE_FORMATS.delete(:default)
else
::Time::DATE_FORMATS[:default] = previous_default
end
In my Rails 5 app I'm trying to globally register a custom Liquid filter, but my filters aren't getting registered.
In my root/lib folder I save:
# lib/liquid_filters.rb
module DatetimeFilters
def previous_business_day(datetime)
Time.previous_business_day(datetime)
end
end
Liquid::Template.register_filter(DatetimeFilters)
I then run it through liquid:
# app/controllers/my_controller.rb
template = Liquid::Template.parse('{{ datetime | previous_business_day }}')
result = template.render({'datetime' => Time.parse('2018-01-21 00:00:00 +0100')})
The expected output is:
'2018-01-19 00:00:00 +0100'
But instead the output is simply the variable, unaltered:
'2018-01-21 00:00:00 +0100'
Whatever I put into the previous_business_day method is ignored, which leads me to the conclusion that the filter isn't registered at all.
What am I doing wrong?
the problem is that when you apply the filter previous_business_day it is already as a string, so try this:
module DatetimeFilters
def previous_business_day(datetime)
Time.previous_business_day(Time.parse(datetime))
end
end
I want to update the :position_status in the model based on if :position_date which is equal Date.today, let say I have the :position_date which is Mon, 26 Oct 2017 stored in the database, if the Date.today is on that particular date, then update the :position_status
I have tried to change the position_date to today date to see if it will update or not, but the :position_date was not updated.
attr_accessor :update_position_status
def update_position_status
if Date.today == self.position_date
self.update_attribute(:position_status => 'Filled')
end
end
update_attribute(name, value)
http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute
update_attribute updates a single attribute and skips validations. update_attributes takes a hash of attributes and performs validations.
So the corrected code would be:
def update_position_status!
update_attribute(:position_status, 'Filled') if self.position_date.today?
end
The name should end with ! since it mutates (changes) the object.
However updating the records one by one is not a particularly scalable solution. Instead you want to select the all the records by date and do a mass update:
# MySQL
Thing.where("DATE(position_date)=CURDATE()")
.update_all(position_status: 'Filled')
# Postgres
Thing.where("date_trunc('day', position_date) = current_date()")
.update_all(position_status: 'Filled')
Yes update_attribute requires two arguments.
The correct syntax is:
self.update_attribute(:position_status, 'Filled')
I need to be able to receive a user-input timestamp, with an optional time zone component, validate that is it a valid ISO 8601 time representation, and parse it according to the user's configured time zone.
I'm using Rails 4.2.6 on Ruby 2.3. I had hoped that Time.zone (ActiveSupport::TimeZone) would have an equivalent implementation to Time::iso8601 so that I could rescue ArgumentError exceptions to determine if the user input was a valid ISO 8601 time representation. Then I could do something like:
user_time_zone = 'America/Los_Angeles' # Would actually be from user's stored settings
params = {when: '2016-04-01T01:01:01'} # Would actually be from user input
# Would actually use Time::use_zone in around_action filter
Time.use_zone(user_time_zone) do
timestamp = Time.zone.iso8601 params[:when]
end
But, alas, no such method exists. And I can't find an equivalent one.
I can't use Time.zone.parse, because it treats ambiguous dates as valid (e.g. Time.zone.parse '04/11/16' # => Tue, 16 Nov 0004 00:00:00 LMT -07:52).
The best alternative I've been able to come up with so far is:
Time.use_zone(user_time_zone) do
old_tz = ENV['TZ']
ENV['TZ'] = Time.zone.name
timestamp = Time.iso8601 params[:when] # => 2016-04-01 01:01:01 -0700
ENV['TZ'] = old_tz
end
But this is ugly, messing around with an environment variable this way doesn't feel proper, and it and certainly isn't Rails-like. How can I validate and parse the time according to the user's time zone in a Rails way?
I suggest that you simply split the assignment into two steps: validate the ISO8601 format first and if valid, parse it:
user_time_zone = 'America/Los_Angeles'
params = { when: '2016-04-01T01:01:01' }
begin
Time.iso8601(params[:when]) # raises ArgumentError if format invalid
rescue ArgumentError => e
puts "invalid time format"
return
end
Time.use_zone(user_time_zone) do
timestamp = Time.zone.parse(params[:when])
end
I think you can still use an around_action for your use case. That's what I use and it works well for me.
In my ApplicationController I have:
class ApplicationController < ActionController::Base
around_action :set_time_zone
def set_time_zone
old_time_zone = Time.zone
Time.zone = user_time_zone
yield
ensure
Time.zone = old_time_zone
end
end
Any calls to Time will use the user's time zone within the scope of the request.
I'm implementing the Timezone support in Rails 2.1+, and I've run into an apparent bug in the way the data is pulled from the db. Let me try to set it up.
The "deals" model contains an "offer_date" datetime field. Let's say I have two records with these offer_dates:
Deal 1: 2009-12-29 23:59:59 -0500
Deal 2: 2009-12-28 23:59:59 -0500
This is correct: dates should mark the last second of its given day.
Now, when it comes time to find the deal for today, I have the following AR query:
#deal = Deal.first(:conditions=>['offer_date > ?', Time.now.beginning_of_day], :order=>"offer_date ASC")
In this case, although it's the 29th today, it returns the record ostensibly for the 28th. Why? Here's what happens in the console:
>> #deal = Deal.first(:conditions=>['offer_date > ?', Time.now.beginning_of_day], :order=>"offer_date ASC")
=> #<Deal id: 2, <blah blah blah...>, offer_date: "2009-12-29 04:59:00">
It's shifting the time forward by 5 hours, putting yesterday's day into the next. But when I do this:
>> #deal.offer_date
=> Mon, 28 Dec 2009 23:59:00 EST -05:00
I get the right time. What the heck??? Suffice to say, I need that date to work properly, but I can't figure out where my issue is. Any assistance you can provide would be greatly appreciated!
See my prior rsponse on Time vs Time.zone for AR date queries.
Instead of using Time.now, try using Time.zone.now
Of course you have to set this and everything. This tutorial seems helpful.
the Time class doesn't care about the time zone in the implementation of #to_s, therefore you have to use
Time.zone.now # or any other TimeWithZone object
in your finders/named_scopes/find calls. Or you could read through http://marklunds.com/articles/one/402 and then put
module ActiveRecord
module ConnectionAdapters # :nodoc:
module Quoting
# Convert dates and times to UTC so that the following two will be equivalent:
# Event.all(:conditions => ["start_time > ?", Time.zone.now])
# Event.all(:conditions => ["start_time > ?", Time.now])
def quoted_date(value)
value.respond_to?(:utc) ? value.utc.to_s(:db) : value.to_s(:db)
end
end
end
end
into your environment.rb or into an initializer.