-
Notifications
You must be signed in to change notification settings - Fork 21.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Time.at(time) != time #38831
Comments
Just for the context, the difference is in
This does't happen with t=Time.now
t.nsec # => 328689000
Time.at(t).nsec # => 328689000 The same applies to Rails 4.2 and earlier Rubies. |
@pirj Illustrates why you don't get the expected true valued when comparing these two object. Also The question is, why exactly is this a problem for you? What is the the use case where you would do this? You use two different methods which instantiate two different object types as shown here: current_time = Time.current
at_time_called_on_current_time = Time.at(current_time)
current_time.class
=> ActiveSupport::TimeWithZone < Object
at_time_called_on_current_time.class
=> Time < Object When writing to the database for example, it's nearly impossible to write 2 records at the exact same nanosecond. Here's an example where I'll multithread and write 2 new users at the "exact same time", but not really. threads = []
2.times do
threads << Thread.new do |i|
u = User.new.save(validate: false)
end
end
threads.each { |thr| thr.join }
time_stamps = User.u.all.order(created_at: :desc).limit(2).pluck(:created_at)
(0.5ms) SELECT "users"."created_at" FROM "users" ORDER BY "users"."created_at" DESC LIMIT 2
=> [
[0] Sun, 29 Mar 2020 20:39:14 EDT -04:00,
[1] Sun, 29 Mar 2020 20:39:14 EDT -04:00
] And here it appears those two records were created at the same time. But they were not! time_stamps[0] == time_stamps[1]
=> false But this is factually true as can be illustrated in Postgres for example: SELECT users.created_at FROM "users" ORDER BY "users"."created_at" DESC LIMIT 2
test_development-# ;
created_at
----------------------------
2020-03-30 00:39:14.612592
2020-03-30 00:39:14.610636 The point is, |
@lacostenycoder I understand what you mean, but I disagree with some of the statements.
t.datetime "created_at"
t.datetime "updated_at" is seconds, and
time = User.last.updated_at
time.class # => ActiveSupport::TimeWithZone
time.is_a? Time # => true so I would expect it to work in the same way (even though it's delegation, not inheritance). Especially keeping in mind that The problem, again, is the incorrect behavior of The root cause of the issue might be this:
but I'm pretty sure it can be worked around to provide exact the same value on the double conversion of Side note 1 I personally strongly disagree with the solution to this problem introduced here #35713, especially in the part of rounding the values of all time objects found in the payload. Side note 2 as per your example with threads, even if |
@pirj good points. So what is the recommended approach to fixing this? I still don't see a use case here. But I did run into this problem recently which led to confusion when were expecting time stamps to match but got false when comparing objects. I had the idea that perhaps adding a configuration setting? Looks like this didn't fix the problem I guess this is the problem From: activesupport-4.2.11.1/lib/active_support/core_ext/time/calculations.rb @ line 30:
Owner: #<Class:Time>
Visibility: public
Number of lines: 3
def current
::Time.zone ? ::Time.zone.now : ::Time.now
end |
Nice find @lacostenycoder ! Tests added along with the fix seem to be unsufficient to prevent this regression from happening. |
@pirj any ideas on the best approach to take here? |
Found this workaround. It seems that t = Time.current
t.nsec == Time.at(t.to_r).nsec # => true There's a Ruby core spec that covers it "roundtrips a Rational produced by #to_r" do
t = Time.now()
t2 = Time.at(t.to_r)
t2.should == t
t2.usec.should == t.usec
t2.nsec.should == t.nsec
end It seems to copy over the underlying time data with no modifications if the object passed as an argument to else if (IsTimeval(time)) {
struct time_object *tobj, *tobj2;
GetTimeval(time, tobj);
t = time_new_timew(klass, tobj->timew);
GetTimeval(t, tobj2);
TZMODE_COPY(tobj2, tobj);
} Also, the spec promises that the value would be describe "with an argument that responds to #to_r" do
it "coerces using #to_r" do
o = mock_numeric('rational')
o.should_receive(:to_r).and_return(Rational(5, 2))
Time.at(o).should == Time.at(Rational(5, 2))
end
end I don't think this (5/2) gives enough certainty regarding precision though. 😕 There's another spec that is intended to compare the equality of the argument with the result of it "creates a new time object with the value given by time" do
t = Time.now
Time.at(t).inspect.should == t.inspect
end but again, the precision is left aside, as Time.now.inspect # => "2020-03-30 17:28:56 +0000" I'm not certain if an object of a type Time.current.kind_of? Time # => true but the observation of mismatch in nanoseconds points that it's timew = rb_time_magnify(v2w(num_exact(time)));
t = time_new_timew(klass, timew); v2w(VALUE v)
{
if (RB_TYPE_P(v, T_RATIONAL)) {
if (RRATIONAL(v)->den != LONG2FIX(1))
return WIDEVAL_WRAP(v);
v = RRATIONAL(v)->num;
} and frankly, I can't see any check to whether it responds to
I lack Ruby internals knowledge, but have a strong suspicion that Wondering when |
@pirj excellent research! |
Yet another thing that comes into play: # Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime
# instances can be used when called with a single argument
def at_with_coercion(*args)
...
time_or_number = args.first
if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime)
at_without_coercion(time_or_number.to_f).getlocal
...
end
alias_method :at_without_coercion, :at
alias_method :at, :at_with_coercion So even though t=Time.now
t.nsec == Time.at(t.to_f).nsec # => false
t.nsec # => 148269000
Time.at(t.to_f).nsec # => 148268938 Bingo! Wondering if there are any implications to change it this way: - at_without_coercion(time_or_number.to_f).getlocal
+ at_without_coercion(time_or_number.to_r).getlocal It seems that - if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime)
+ if time_or_number.is_a?(ActiveSupport::TimeWithZone)
+ at_without_coercion(time_or_number.to_r).getlocal
+ elsif time_or_number.is_a?(DateTime)
at_without_coercion(time_or_number.to_f).getlocal
else
at_without_coercion(time_or_number)
end |
My suspicion regarding Ruby's false promise of converting T=Class.new do
def respond_to?(m, *)
puts m
return true if m == :to_r
super
end
def to_r
@r
end
def initialize(r)
@r = r
end
end
Time.at(T.new(Rational(500000000, 3)))
🦆 😕 🤷♂ |
Filed https://bugs.ruby-lang.org/issues/17131 I intend to send a pull request to make use of |
@pirj Well done on the bug fix! The battle is won and the spoils are yours. The cheque is in your hand, but you are yet to cash it?! Any chance you could push through the amendments you have suggested? |
Fixed by #40448. |
Steps to reproduce
Whyle discussing rspec/rspec-rails#2301 we noticed the following:
Shouldn't it be
true
?And:
Timezone seems to be broken after
Time.at
. Maybe that is the reason why comparison isfalse
above?Is this something for Rails to fix or is that correct behavior?
System configuration
Rails version: 6.0.2.2
Ruby version: 2.7.0
The text was updated successfully, but these errors were encountered: