Activerecord: Converting strings to times in the local time - activerecord

I've read quite a few articles, done my homework. I have all times stored as UTC, each user sets their own time zone, etc. Here's the problem I'm having:
Time.zone
=> GMT-05:00 Eastern Time US Canada
t = Ticket.first
t.hold_until = "Jan 1, 2012 9:00PM"
t.save!
t.hold_until
=> Sun, 01 Jan 2012 16:00:00 EST -05:00
# notice the above time lost 5 hours
The issue is that the string is from a POST request supplied by the user. Doesn't it make the most sense to assume user input is in their local time, not UTC. Am I missing something or shouldn't that be the ActiveRecord default? Whats the proper way to do this, beyond having to convert the time in all of my controllers. That just seems wrong and not very DRY.

I figured out the issue and it has to do with poorly written code within ActiveRecord (surprise!). Anyways, ActiveRecord::Base.define_attribute_methods gets called lazily via method missing. I had this method defined like so:
def hold_until=(value)
# do some stuff here
super
end
So ActiveRecord was skipping that method thinking it was defined. It shouldn't matter, but due to poor design it does. AR has a variety of different ways to define methods based on their column type. Calling super side steps this and goes right to a general "write_attribute" call. The only solution is to convert the time yourself. You can't call define_attribute_methods before you include the modules because of all the alias_method_chain bullshit. And you can't leverage the code they've already written because its in a class_eval block. I would recommend datamapper if at all possible.

Related

What is a simple way in Ruby to show the date and time?

I would like something in Ruby roughly equivalent to time.asctime() in Python:
import time
print(time.asctime())
outputs:
Sun Sep 11 10:12:48 2022
I'd like to avoid having to use strftime and having to remember or look up the formats. Also, ideally I'd like both day of the week (e.g., Sun) and the UTC timezone difference (e.g., -0400), but I'd settle for just day of the week.
puts Time.now.asctime
outputs:
Sun Sep 11 10:24:46 2022
Simple String Output
Ruby supports lots of Time, Date, and DateTime objects and output formats. While I think the first answer is closer to the output format you want, the following is potentially simpler and possibly sufficient for many needs when just considering standard output or standard error:
p Time.now.to_s
#=> 2022-09-11 14:10:57 -0400
# using interpolation
p "Time: #{Time.now.to_s}"
#=> "Time: 2022-09-11 14:15:51 -0400"
Other Considerations
Note that if you want to use the results for any sort of comparison or calculation, you'll likely need to convert the result to one of the three object types described above. That's the main reason I mention them. Unless it's just printing to the screen, you should think about how you plan to use the result before deciding which of the objects will be most useful for you.

Ruby, get difference between GMT of timezone

I'm going to get difference between GMT (for example: +09:00) in Ruby, using TZInfo gem. I'm reading documentation and I can't find the solution. I tried:
tz = TZInfo::Timezone.get('Asia/Shanghai')
puts tz.strftime('%:z')
But it returns +00:00, that's not correct, why? It's looks like it still using London time.
This should do it, but as someone pointed out, you can't just use the location, you need to check for daylight savings etc.
tz = TZInfo::Timezone.get('Asia/Shanghai')
puts sprintf("%02d:%02d", *tz.current_period.offset.utc_offset.divmod(3600))
A time zone by itself does not have the notion of a time difference because the time difference may change during the period within the year (some time zones have summer time). You need to assign particular date time in order to get a meaningful time difference.

Millisecond resolution of DateTime in Ruby

I have a string like 2012-01-01T01:02:03.456 that I am storing in a Postgres database TIMESTAMP using ActiveRecord.
Unfortunately, Ruby seems to chop off the milliseconds:
ruby-1.9.3-rc1 :078 > '2012-12-31T01:01:01.232323+3'.to_datetime
=> Mon, 31 Dec 2012 01:01:01 +0300
Postgrs supports microsecond resolution. How can I get my timestamp to be saved accordingly? I need at least millisecond resolution.
(PS Yes I could hack in a milliseconds integer column in postgres; that kind of defeats the whole purpose of ActiveRecord.)
UPDATE:
The very helpful responses showed that Ruby's DateTime is not chopping off milliseconds; using #to_f shows it. But, doing:
m.happened_at = '2012-01-01T00:00:00.32323'.to_datetime
m.save!
m.reload
m.happened_at.to_f
Does drop the milliseconds.
Now, the interesting thing is that created_at does show milliseconds, both in Rails and Postgres. But other timestamps fields (like happened_at above) don't. (Perhaps Rails uses a NOW() function for created_at as opposed to passing in a DateTime).
Which leads to my ultimate question:
How can I get ActiveRecord to preserve millisecond resolution on timestamp fields?
ActiveRecord should preserve the full precision from the database, you're just not looking at it properly. Use strftime and the %N format to see the fractional seconds. For example, psql says this:
=> select created_at from models where id = 1;
created_at
----------------------------
2012-02-07 07:36:20.949641
(1 row)
and ActiveRecord says this:
> Model.find(1).created_at.strftime('%Y-%m-%d %H:%M:%S.%N')
=> "2012-02-07 07:36:20.949641000"
So everything is there, you just need to know how to see it.
Also note that ActiveRecord will probably give you ActiveSupport::TimeWithZone objects rather than DateTime objects but DateTime preserves everything too:
> '2012-12-31T01:01:01.232323+3'.to_datetime.strftime('%Y-%m-%d %H:%M:%S.%N')
=> "2012-12-31 01:01:01.232323000"
Have a look at connection_adapters/column.rb in the ActiveRecord source and check what the string_to_time method does. Your string would go down the fallback_string_to_time path and that preserves fractional seconds as near as I can tell. Something strange could be going on elsewhere, I wouldn't be surprised given the strange things I've seen in the Rails source, especially the database side of things. I'd try converting the strings to objects by hand so that ActiveRecord will keeps its hands off them.
Changing m.happened_at = '2012-01-01T00:00:00.32323'.to_datetime in the code above to m.happened_at = '2012-01-01T00:00:00.32323' solves the problem, though I have no idea why.
I ended up here when I was suffering from using the RVM provided binary Ruby 2.0.0-p247 on OS X (Mavericks) which was causing rounding to whole values of seconds when retrieving times from Postgres. Rebuilding Ruby myself (rvm reinstall 2.0.0 --disable-binary) solved the issue for me.
See https://github.com/wayneeseguin/rvm/issues/2189 which I found via https://github.com/rails/rails/issues/12422.
I recognise that this is not THE answer to this issue but I hope this note might help someone struggling with it.
to_datetime does not destroy millisecond resolution of data - it's simply hidden because DateTime#to_s doesn't display it.
[1] pry(main)> '2012-12-31T01:01:01.232323+3'.to_datetime
=> Mon, 31 Dec 2012 01:01:01 +0300
[2] pry(main)> '2012-12-31T01:01:01.232323+3'.to_datetime.to_f
=> 1356904861.232323
That said, I suspect that ActiveRecord is mistakenly hiding that information when persisting the data; remember that it is database-agnostic, so it takes approaches that are guaranteed to work across all of its database targets. While Postgres supposed microsecond information in timestamps, MySQL does not, so I suspect AR selects for the lowest common denominator. I couldn't be sure without getting into the guts of AR. You may need a Postgres-specific monkeypatch to enable this behavior.

How to get time_zone_options_for_select with DST offsets?

The ActionView::Helpers::FormOptionsHelper provides the time_zone_options_for_select to get a list of options for a select control that includes all of the timezones with their UTC offset. The problem I'm having is how to get it to display the correct offset for when daylight savings time is in effect?
For instance U.S. Mountain time is usually -7 UTC but during the summer it's effectively -6 UTC. Is there a way to have that list correctly reflect that?
TL;DR
The time zone data you are receiving is "correct" because ActiveSupport::TimeZone and TZInfo::Timezone instances do not make assumptions about the current date, and therefore applying DST to them doesn't make "sense" in the context of the responsibility of those objects.
You notice the problem because the default model for time_zone_options_for_select, ActiveSupport::TimeZone, unfortunately returns the offset string when calling #to_s, which, if the location is currently is observing DST, will be incorrect. There is no way to fix the string values generated in the options, or even remove the offset from them.
If this isn't acceptable you'll need to skip on using time_zone_options_for_select, and use options_for_select instead, and generate the options yourself.
Some investigation
time_zone_options_for_select uses ::ActiveSupport::TimeZone as the default model parameter, so passing it in manually will not change your results. In order to construct options for a select box, that method will construct an array of tuples in the format of [time_zone.to_s, time_zone.name], for the purpose of passing it to the more generic options_for_select method. time_zone, in this case, is an instance of ::ActiveSupport::TimeZone.
The important factor here is that this time zone instance object is, conceptually, completely unrelated/divorced from the idea of "the current date". The definition of a time zone (strictly speaking) has nothing to do with the current date. We can confirm this "not using DST" issue like so:
::ActiveSupport::TimeZone.all.find { |tz| tz.name == "Adelaide" }.utc_offset
=> 34200 # 9 hours and 30 minutes, in seconds
Adelaide's non-DST time zone is ACST (Australian Central Standard Time) which is GMT+9.5. Currently (as in the time of writing), Adelaide is in DST which means they are on ACDT (Australian Central Daylight Time), which is GMT+10.5.
::ActiveSupport::TimeZone.all.find { |tz| tz.name == "Adelaide" }.now.utc_offset
=> 37800 # 10 hours and 30 minutes, in seconds
The crucial difference here is essentially what I've outlined above - the ActiveSupport::TimeZone instance is just not concerned with the current date. The class itself is a convenience wrapper around a TZInfo::DataTimezone instance, which has similar opinions on the current date - none.
If you noticed, in the second code snippet above, we called #now on the time zone object before calling #utc_offset. This returns an ActiveSupport::TimeWithZone instance, which includes information about the time zone, as well as the current date - and therefore we get an offset which reflects the fact that the current date should include the DST offset.
So, the only problem here is that including the UTC offset string in the return value of #to_s on ActiveSupport::TimeZone instances is sort of misleading in this instance. It's included because it's the "base" UTC offset for that time zone.
Resources:
Rails 6.1 time_zone_options_for_select implementation
Related GitHub issue, rails/rails#7297
Related GitHub pull request, rails/rails#22243
I had similar problem but ended up using this
time_zone_select('time_zone', TZInfo::Timezone.us_zones,
:default => "America/Los_Angeles",
:model => TZInfo::Timezone
Did you find a better solution?

How to get UTC timestamp in Ruby?

How to get UTC timestamp in Ruby?
You could use: Time.now.to_i.
time = Time.now.getutc
Rationale: In my eyes a timestamp is exactly that: A point in time. This can be accurately represented with an object. If you need anything else, a scalar value, e.g. seconds since the Unix epoch, 100-ns intervals since 1601 or maybe a string for display purposes or storing the timestamp in a database, you can readily get that from the object. But that depends very much on your intended use.
Saying that »a true timestamp is the number of seconds since the Unix epoch« is a little missing the point, as that is one way of representing a point in time, but it also needs additional information to even know that you're dealing with a time and not a number. A Time object solves this problem nicely by representing a point in time and also being explicit about what it is.
The default formatting is not very useful, in my opinion. I prefer ISO8601 as it's sortable, relatively compact and widely recognized:
>> require 'time'
=> true
>> Time.now.utc.iso8601
=> "2011-07-28T23:14:04Z"
if you need a human-readable timestamp (like rails migration has) ex. "20190527141340"
Time.now.utc.to_formatted_s(:number) # using Rails
Time.now.utc.strftime("%Y%m%d%H%M%S") # using Ruby
Usually timestamp has no timezone.
% irb
> Time.now.to_i == Time.now.getutc.to_i
=> true
What good is a timestamp with its granularity given in seconds? I find it much more practical working with Time.now.to_f. Heck, you may even throw a to_s.sub('.','') to get rid of the decimal point, or perform a typecast like this: Integer(1e6*Time.now.to_f).
Time.utc(2010, 05, 17)
time = Time.zone.now()
It will work as
irb> Time.zone.now
=> 2017-12-02 12:06:41 UTC
The proper way is to do a Time.now.getutc.to_i to get the proper timestamp amount as simply displaying the integer need not always be same as the utc timestamp due to time zone differences.

Resources