Time difference in rspec - ruby-on-rails

I'm writing a simple class to parse strings into relative dates.
module RelativeDate
class InvalidString < StandardError; end
class Parser
REGEX = /([0-9]+)_(day|week|month|year)_(ago|from_now)/
def self.to_time(value)
if captures = REGEX.match(value)
captures[1].to_i.send(captures[2]).send(captures[3])
else
raise InvalidString, "#{value} could not be parsed"
end
end
end
end
The code seems to work fine.
Now when I try my specs I get a time difference only in year and month
require 'spec_helper'
describe RelativeDate::Parser do
describe "#to_time" do
before do
Timecop.freeze
end
['day','week','month','year'].each do |type|
it "should parse #{type} correctly" do
RelativeDate::Parser.to_time("2_#{type}_ago").should == 2.send(type).ago
RelativeDate::Parser.to_time("2_#{type}_from_now").should == 2.send(type).from_now
end
end
after do
Timecop.return
end
end
end
Output
..FF
Failures:
1) RelativeDate::Parser#to_time should parse year correctly
Failure/Error: RelativeDate::Parser.to_time("2_#{type}_ago").should == 2.send(type).ago
expected: Wed, 29 Aug 2012 22:40:14 UTC +00:00
got: Wed, 29 Aug 2012 10:40:14 UTC +00:00 (using ==)
Diff:
## -1,2 +1,2 ##
-Wed, 29 Aug 2012 22:40:14 UTC +00:00
+Wed, 29 Aug 2012 10:40:14 UTC +00:00
# ./spec/lib/relative_date/parser_spec.rb:11:in `(root)'
2) RelativeDate::Parser#to_time should parse month correctly
Failure/Error: RelativeDate::Parser.to_time("2_#{type}_ago").should == 2.send(type).ago
expected: Sun, 29 Jun 2014 22:40:14 UTC +00:00
got: Mon, 30 Jun 2014 22:40:14 UTC +00:00 (using ==)
Diff:
## -1,2 +1,2 ##
-Sun, 29 Jun 2014 22:40:14 UTC +00:00
+Mon, 30 Jun 2014 22:40:14 UTC +00:00
# ./spec/lib/relative_date/parser_spec.rb:11:in `(root)'
Finished in 0.146 seconds
4 examples, 2 failures
Failed examples:
rspec ./spec/lib/relative_date/parser_spec.rb:10 # RelativeDate::Parser#to_time should parse year correctly
rspec ./spec/lib/relative_date/parser_spec.rb:10 # RelativeDate::Parser#to_time should parse month correctly
The first one seems like a time zone issue but the other one is even a day apart? I'm really clueless on this one.

This is a fascinating problem!
First, this has nothing to do with Timecop or RSpec. The problem can be reproduced in the Rails console, as follows:
2.0.0-p247 :001 > 2.months.ago
=> Mon, 30 Jun 2014 20:46:19 UTC +00:00
2.0.0-p247 :002 > 2.months.send(:ago)
DEPRECATION WARNING: Calling #ago or #until on a number (e.g. 5.ago) is deprecated and will be removed in the future, use 5.seconds.ago instead. (called from irb_binding at (irb):2)
=> Wed, 02 Jul 2014 20:46:27 UTC +00:00
[Note: This answer uses the example of months, but the same is true for the alias month as well as year and years.]
Rails adds the month method to the Integer class, returning an ActiveSupport::Duration object, which is a "proxy object" containing a method_missing method which redirects any calls to the method_missing method of the "value" it is serving as a proxy for.
When you call ago directly, it's handled by the ago method in the Duration class itself. When you try to invoke ago via send, however, send is not defined in Duration and is not defined in the BasicObject that all proxy objects inherit from, so the method_missing method of Rails' Duration is invoked which in turn calls send on the integer "value" of the proxy, resulting in the invocation of ago in Numeric. In your case, this results in a change of date equal to 2*30 days.
The only methods you have to work with are those defined by Duration itself and those defined by BasicObject. The latter are as follows:
2.0.0-p247 :023 > BasicObject.instance_methods
=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]
In addition to the instance_eval you discovered, you can use __send__.
Here's the definition of method_missing from duration.rb
def method_missing(method, *args, &block) #:nodoc:
value.send(method, *args, &block)
end
value in this case refers to the number of seconds in the Duration object. If you redefine method_missing to special case ago, you can get your test to pass. Or you can alias send to __send__ as follows:
class ActiveSupport::Duration
alias send __send__
end
Here's another example of how this method_missing method from Duration works:
macbookair1:so1 palfvin$ rails c
Loading development environment (Rails 4.1.1)
irb: warn: can't alias context from irb_context.
2.0.0-p247 :001 > class ActiveSupport::Duration
2.0.0-p247 :002?> def foo
2.0.0-p247 :003?> 'foobar'
2.0.0-p247 :004?> end
2.0.0-p247 :005?> end
=> nil
2.0.0-p247 :006 > 2.months.foo
=> "foobar"
2.0.0-p247 :007 > 2.months.respond_to?(:foo)
=> false
2.0.0-p247 :008 >
You can call the newly defined foo directly, but because BasicObject doesn't implement respond_to?, you can't "test" that the method is defined there. For the same reason, method(:ago) on a Duration object returns #<Method: Fixnum(Numeric)#ago> because that's the ago method defined on value.

Related

Converting time zone of Rails ActiveRecord timestamps

I extended the Time class in my Rails projects so I can easily get the time in NYC:
/lib/extensions.rb .
class Time
# Get NYC time:
def nyc
self.in_time_zone('Eastern Time (US & Canada)')
end
end
Testing it out, looks good:
time_a = Time.now.utc.nyc
=> Sun, 21 Apr 2019 18:42:12 EDT -04:00
The problem is when I pull timestamps from the DB:
time_b = object.created_at.in_time_zone('Eastern Time (US & Canada)')
=> Sun, 21 Apr 2019 17:22:04 EDT -04:00
time_c = object.created_at.nyc
=> Sun, 21 Apr 2019 17:22:04 UTC +00:00
Super confused. Converting the timestamp to EDT works when I use in_time_zone in the console, but not when I use the extension? Even though my extension method works on Time objects I create in console? What's happening here?
(Note: Time instances in Rails are in fact instances of ActiveSupport::TimeWithZone. "TimeWithZone instances implement the same API as Ruby Time instances, so that Time and TimeWithZone instances are interchangeable." - ActiveSupportTimeWithZone)
you would need to patch ActiveSupport::TimeWithZone instead of Time, e.g.
class ActiveSupport::TimeWithZone
def nyc
in_time_zone('Eastern Time (US & Canada)')
end
end
Time.zone.now.nyc # => Mon, 22 Apr 2019 06:44:41 EDT -04:00
User.last.created_at.nyc # => Sun, 21 Apr 2019 13:34:45 EDT -04:00
https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
(edit: I previously said "DateTime" instead of "ActiveSupport::TimeWithZone")

Invalid Time.now on RSpec

How do I set the correct Time on RSpec.
[2] pry(#<RSpec::ExampleGroups::Device::Creation::Security>)> Time.now
=> Fri, 01 Jan 2016 10:00:00 CET +01:00
On rails console I have the correct time:
[1] pry(main)> Time.now
=> 2015-12-11 12:41:36 -0500
I don't want to stub it, because I'm not passing the time to my class. Have the following method in the class I'm testing:
def within_expiration?
Time.now - user.sms_sent_date < time_limit_in_seconds
end
This is not behaving as expected because Time.now is Jan 1, 2016
user.sms_sent_date has right date.
Make sure you don't have any library that is currently altering your Time in the spec.
For example, one of the most commonly used is Timecop. Make sure you don't setup it globally, instead use the blocks or before/after to properly mock the date and teardown the mock when no longer required.

Sleep statement in Ruby doesn't accept any rails convertions

I found this very weird. Why am I not able to do this?:
puts Time.now
x=2
puts x
puts x.class
sleep x
puts Time.now
x=x.seconds
puts x
puts x.class
sleep (x)
puts Time.now
The output I get is:
Mon Oct 01 16:14:58 +0530 2012
2
Fixnum
Mon Oct 01 16:15:00 +0530 2012
2
Fixnum
rake aborted!
can't convert ActiveSupport::Duration into time interval
/Users/hariharanganapathiraman/Documents/MigrationScripts/sample/lib/import/boot.rb:23:in `sleep'
Similarly it doesn't work for .minutes or .days.
That's because ActiveSupport::Duration mimics itself as Fixnum class:
ActiveSupport::Duration.new(3600, [[:seconds, 3600]]).class #=> Fixnum
Use sleep(2.seconds.to_i).
The problem is with ActiveSupport::Duration
It's inherited from BasicObject that have only limited number of methods and does not have class method.
If you take a look in ActiveSupport::Duration implementation you'll see that all methods are proxied(via method_missing) to #value, that's Fixnum in your case.
Hence, since ActiveSupport::Duration has no class method, your class call for a x.seconds
goes to Fixnum.

Ruby String to_time broken?

I would expect Time and to_time to be reflective.
UPDATE
This is the answer, if you add the parameter :local, it doesn't ignore the offset. I find it wierd that it ignores data (the offset) by default, though...
Time.zone.now.to_s.to_time(:local) returns 2012-03-20 14:39:03 +0100
/UPDATE
> Time.zone.now.to_s
=> "2012-03-20 12:50:05 +0100"
> Time.zone.now.to_s.to_time.to_s
=> "2012-03-20 12:50:05 UTC"
# PROBLEM:
# UTC is +0000, meaning that there is 1 hour diff in the above
> Time.zone.now
=> Tue, 20 Mar 2012 12:51:32 CET +01:00
> Time.zone.now.to_time
=> Tue, 20 Mar 2012 12:51:32 CET +01:00
> Time.zone.now.to_json
=> "\"2012-03-20T12:50:36+01:00\""
> Time.zone.now.to_json.to_time
=> 2012-03-20 12:50:36 UTC
I have this problem with JSON messages. Anyway, I wouldn't expect to_time to actually change the time. OK to translate to UTC, probably, but it should adjust time accordingly. Anybody know if this is a known issue or "by design"?
This method, String#to_time, defined in ActiveSupport takes one parameter, form, which can be either :utc (default) or :local. So by default, it always returns a Time in UTC.
To get a Time with timezone:
Time.zone.parse("2012-03-20T12:50:36+01:00")

Why does Date.yesterday counts as Date.today also?

I have the following model and methods:
class UserPrice < ActiveRecord::Base
attr_accessible :price, :purchase_date,
def self.today
where(:purchase_date => Date.today)
end
def self.yesterday
where(:purchase_date => Date.yesterday)
end
Why on my form if I give my date_select field :purchase_date 1/4/2012(yesterday method) it also counts as today(today method) and if I give it 1/5/2012 (today) it is nil?
P.S. I am using Rails 3.0.10 with PostgreSQL.
Update
This is my console returns:
$ rails console --s
Loading development environment in sandbox (Rails 3.0.10)
Any modifications you make will be rolled back on exit
irb(main):001:0> Date.today
=> Wed, 04 Jan 2012
irb(main):002:0> Time.now
=> 2012-01-04 21:28:07 -0500
irb(main):003:0> Time.zone.now
=> Thu, 05 Jan 2012 02:28:18 UTC +00:00
irb(main):004:0> Date.yesterday
=> Wed, 04 Jan 2012
irb(main):005:0>
Now yesterday is screwed up, makes no sense....
This is happening because calculations.rb is calling the "current" method of the Date class for the configured timezone (Defaults to UTC).
If you open the rails console you can see the date at which "Date.yesterday" is calculating on by doing:
Time.zone.today
This will probably show you tomorrow's date. So Date.yesterday for what rails sees as today, is today. ;)
You can work with the Date library more directly by doing:
Date.today.advance(:days => -1)
This will give you yesterday's date like you expect whereas active_support is returning:
Date.current.advance(:days => -1)
If you use the Time class, you'll have access to UTC (or "zulu") time zone rather than using the environment's time zone in Date class.
class UserPrice < ActiveRecord::Base
attr_accessible :price, :purchase_date,
def self.today
where(purchase_date: today_utc_date)
end
def self.yesterday
where(purchase_date: today_utc_date.yesterday)
end
private
def today_utc_date
#today ||= Time.zone.today
end
end
Also, if you need to process an outside date, for example params[:purchase_date]:
Time.parse(params[:purchase_date]).utc.to_date

Resources