Ruby, Rails and difference between two dates - ruby-on-rails

I will let the example speak for it self:
ruby-1.9.2-p0 > DateTime.now
=> Mon, 14 Feb 2011 20:02:49 +0100
ruby-1.9.2-p0 > User.first.created_at
=> Tue, 04 May 2010 07:03:24 CEST +02:00
ruby-1.9.2-p0 > DateTime.now-User.first.created_at
TypeError: expected numeric or date
from /Users/Jacob/.rvm/rubies/ruby-1.9.2-p0/lib/ruby/1.9.1/date.rb:1356:in `-'
from /Users/Jacob/.rvm/gems/ruby-1.9.2-p0#loyaltric_template/gems/activesupport-3.0.3/lib/active_support/core_ext/date/calculations.rb:98:in `minus_with_duration'
from (irb):47
from /Users/Jacob/.rvm/gems/ruby-1.9.2-p0#loyaltric_template/gems/railties-3.0.3/lib/rails/commands/console.rb:44:in `start'
from /Users/Jacob/.rvm/gems/ruby-1.9.2-p0#loyaltric_template/gems/railties-3.0.3/lib/rails/commands/console.rb:8:in `start'
from /Users/Jacob/.rvm/gems/ruby-1.9.2-p0#loyaltric_template/gems/railties-3.0.3/lib/rails/commands.rb:23:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
ruby-1.9.2-p0 >
What?!?

.created_at returns an ActiveSupport::TimeWithZone object. Try
DateTime.now - User.first.created_at.to_datetime

You can instead use
Time.now - User.first.created_at
This will report the number of seconds between now and your first user creation time.

Related

Why does Date.strptime not throw ArgumentError in rails console

In my local rails console passing 9/31/2011 returns 10/1/2011
[1] pry(main)> Date.strptime("09/31/2011", '%m/%d/%Y')
=> Sat, 01 Oct 2011
In heroku console it throws an ArgumentError:
irb(main):002:0> Date.strptime("09/31/2011", '%m/%d/%Y')
ArgumentError: invalid date
from (irb):2:in `strptime'
Double check that your Gemfile dev dependencies don't include something that could mess with Date's strptime function.
For example, the Timecop gem sets an alias for Date.strptime to this method:
https://www.rubydoc.info/gems/timecop/Date.strptime_with_mock_date
Which just ends up calling Time.strptime(str, fmt).to_date.
This behavior looks very similar to what you reported:
irb(main):001:0> Date.strptime("09/31/2011", '%m/%d/%Y')
ArgumentError: invalid date
from (irb):1:in 'strptime'
irb(main):002:0> Time.strptime("09/31/2011", '%m/%d/%Y').to_date
=> Sat, 01 Oct 2011

Can't set timezone using abbreviation

I can't set timezone on Rails using its abbreviation, for example:
>> Time.zone = 'BRT'
ArgumentError: Invalid Timezone: BRT
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activesupport-3.2.21/lib/active_support/core_ext/time/zones.rb:61:in `rescue in find_zone!'
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activesupport-3.2.21/lib/active_support/core_ext/time/zones.rb:53:in `find_zone!'
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activesupport-3.2.21/lib/active_support/core_ext/time/zones.rb:37:in `zone='
from (irb):14
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/railties-3.2.21/lib/rails/commands/console.rb:47:in `start'
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/railties-3.2.21/lib/rails/commands/console.rb:8:in `start'
from /home/braulio/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/railties-3.2.21/lib/rails/commands.rb:41:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
This is necessary as some systems (android and some browsers) report timezone using the abbreviation.
The list of abbreviations can be found at http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations
jstimezone was reporting timezone using abbreviations. It is also quite buggy and unmaintained (https://bitbucket.org/pellepim/jstimezonedetect/issues?status=new&status=open). It is simpler to just use standard javascript:
var offset = - new Date().getTimezoneOffset()/60
Then call on document ready:
$.cookie("browser.tzoffset", offset, { expires: 30, path: '/' })
Then in rails use around_filter in ApplicationController:
def set_time_zone
return yield unless (utc_offset = cookies['browser.tzoffset']).present?
utc_offset = utc_offset.to_i
gmt_offset = if utc_offset == 0 then nil elsif utc_offset > 0 then -utc_offset else "+#{-utc_offset}" end
Time.use_zone("Etc/GMT#{gmt_offset}"){ yield }
rescue ArgumentError
yield
end
This localizes all dates for users, independently where he/she is. In Brazil we have multiple timezones, for example.
PS: ActiveSupport::TimeZone[utc_offset.to_i] can't be used as it uses DST, see https://github.com/rails/rails/issues/20504
PS: You can also use moment: moment.parseZone(Date.now()).utcOffset()/60 or moment().format('zz')
You don't have to use around_filter.
Put this in before_action
Time.zone = "Etc/GMT#{gmt_offset}"
(Time.zone is thread local. It's safe to change.)

Time difference in rspec

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.

Comparison between two ActiveSupport::TimeWithZone objects fails

In my test suite, I have a failing test.
expected[0]['date'] comes from SomeModel.first.created_at
In a debugging console, I have the following:
> expected[0]['date']
=> Tue, 25 Mar 2014 16:01:45 UTC +00:00
> res[0]['date']
=> Tue, 25 Mar 2014 16:01:45 UTC +00:00
> res[0]['date'] == expected[0]['date']
=> false # wtf
> res[0]['date'].class
=> ActiveSupport::TimeWithZone
> expected[0]['date'].class
=> ActiveSupport::TimeWithZone
>
How is this possible ?
I've tried to reproduce this problem (I tought maybe the == operator on TimeWithZone checks the reference, or something like this, but no...) :
> t1 = Time.zone.at(0)
=> Thu, 01 Jan 1970 00:00:00 UTC +00:00
> t2 = Time.zone.parse(t1.to_s)
=> Thu, 01 Jan 1970 00:00:00 UTC +00:00
> t1 == t2
=> true
> t1.class
=> ActiveSupport::TimeWithZone
> t2.class
=> ActiveSupport::TimeWithZone
Edit: More tests...
> res[0]['date'].eql?(expected[0]['date'])
=> false
> res[0]['date'].zone
=> "UTC"
> expected[0]['date'].zone
=> "UTC"
> expected[0]['date'].getlocal
=> 2014-03-25 16:01:45 +0000
> res[0]['date'].getlocal
=> 2014-03-25 16:01:45 +0000
> res[0]['date'].hash
=> -3455877575500291788
> expected[0]['date'].hash
=> -3819233736262144515
>
> t1.hash
=> 2279159074362527997
> t2.hash
=> 2279159074362527997
# inspect...
> expected[0]['date'].inspect
=> "Tue, 25 Mar 2014 16:39:01 UTC +00:00"
> res[0]['date'].inspect
=> "Tue, 25 Mar 2014 16:39:01 UTC +00:00"
Looks like the comparison is based on the hash object. Why res and expected have different hashes ?
Answer #1 rake db:test:prepare
First, attempt dropping the test database and recreating it, and then running rake db:test:prepare. This has resolved this issue for me in the past I know this is a bit of a lame answer, but it is worth a shot.
Answer #2 Spring + Rspec + Shoulda matchers
If having this issue after installing Spring, please checkout this Github Thread, which can cause tests to fail:
https://github.com/rails/spring/issues/209
This issue only started occurring for me after adding Spring to my project.
Adding gem 'shoulda', require: false and manually adding require 'shoulda/matchers' to my spec_helper.rb resolved the issues
Answer #3 Timecop
If still having issues, checkout the Timecop gem and freeze time around date comparisons.
https://github.com/travisjeffery/timecop

Comparing time or ditching date in ruby on rails

I have some time defined from my database, and this is how it looks:
ruby-1.9.2-p290 :017 > djel.smjena.pocetak1.to_time
=> 2000-01-01 08:00:00 +0100
and that is ok, it assigned me 2000-1-1
also, I got something that happened in some datetime
ruby-1.9.2-p290 :019 > dog.pocetak
=> Thu, 25 Aug 2011 08:18:00 UTC +00:00
So I was hoping, that .to_time would ditch my date, but that does not
happen
ruby-1.9.2-p290 :020 > dog.pocetak.to_time
=> Thu, 25 Aug 2011 08:18:00 UTC +00:00
so, now, comparing if something happened before 8:00 is useless.
So, how can I compare that? is there a way to set dog.pocetak to
2000-01-01 without touch clock?
thank you
p.s. also, I thought of creating new time variable, only to get from old variable hours and minutes, but this methods dont work?
ruby-1.9.2-p290 :059 > dog.pocetak.hour
=> 8
but
ruby-1.9.2-p290 :060 > dog.pocetak.minute
NoMethodError: undefined method `minute' for 2011-08-25 08:18:00 UTC:Time
from /home/dorijan/.rvm/gems/ruby-1.9.2-p290/gems/activesupport-3.0.10/lib/active_support/time_with_zone.rb:322:in `method_missing'
from (irb):60
from /home/dorijan/.rvm/gems/ruby-1.9.2-p290/gems/railties-3.0.10/lib/rails/commands/console.rb:44:in `start'
from /home/dorijan/.rvm/gems/ruby-1.9.2-p290/gems/railties-3.0.10/lib/rails/commands/console.rb:8:in `start'
from /home/dorijan/.rvm/gems/ruby-1.9.2-p290/gems/railties-3.0.10/lib/rails/commands.rb:23:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
ruby-1.9.2-p290 :061 > dog.pocetak.minutes
NoMethodError: undefined method `minutes' for 2011-08-25 08:18:00 UTC:Time
from /home/dorijan/.rvm/gems/ruby-1.9.2-p290/gems/activesupport-3.0.10/lib/active_support/time_with_zone.rb:322:in `method_missing'
from (irb):61
from /home/dorijan/.rvm/gems/ruby-1.9.2-p290/gems/railties-3.0.10/lib/rails/commands/console.rb:44:in `start'
from /home/dorijan/.rvm/gems/ruby-1.9.2-p290/gems/railties-3.0.10/lib/rails/commands/console.rb:8:in `start'
from /home/dorijan/.rvm/gems/ruby-1.9.2-p290/gems/railties-3.0.10/lib/rails/commands.rb:23:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
really frustrating :)
With ActiveSupport and Time.change you can reset the year, month and day if you like:
> t = Time.now
=> Sun Aug 21 00:46:29 +0000 2011
> t.change(:month => 1, :day => 1, :year => 2000)
=> Sat Jan 01 00:46:29 +0000 2000
This way you could compare the "times" between each other, if they all were reset to the same date. Not sure if this is a good solution though, depends on what you really are looking for.
EDIT:
As per mu's suggestion you could also take a look at the time data type.
To get the minutes from a Time object, you want min not minutes. You can't have a Time instance that's just a "time of day" (i.e. no year, month, ...) but you can use strftime to get a string version that will compare properly:
tod = Time.now.strftime('%H:%M:%S')
# "17:07:23"
if(t1.strftime('%H:%M:%S') == t2.strftime('%H:%M:%S'))
# Same time of day (to one second resolution)
end
Or you could compare the individual hour, min, and sec components:
if(t1.hour == t2.hour && t1.min == t2.min && t1.sec == t2.sec)
# Same time of day (to one second resolution)
end
Which approach you take depends, as usual, on your specific situation and what else is going in in that vicinity.

Resources