How do I work with Time in Rails? - ruby-on-rails

I've been pulling my hair out trying to work with Time in Rails. Basically I need to set all time output (core as well as ActiveSupport) to the server's local time -- no GMT, no UTC, etc. I've seen various posts relating to Time, but they usually involve someone's need to set it for each user. Mine isn't nearly as complex, I simply want consistency when I use any Time object. (I'd also appreciate not receiving errors every 3 seconds telling me that I can't convert a Fixnum (or some other type) to string -- it's Ruby, just do it!)
I also seem to be getting drastically different times for Time.new vs the ActiveSupport 1.second.ago. Anyway, does anyone have any quality suggestions as regards working with Time in Rails?

If you just want Time objects to be consistent, then why not stick with UTC? I just tried Time.new and 1.second.ago using script/console and I get the same output (give or take a second for typing the command). How are you doing it?

Somewhere in your initializers, define the format(s) that you want to use.
ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!(:default => '%m/%d/%Y %H:%M')
ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!(:my_special_format => '%H:%M %p')
Then when you want to print a Time object, it works like the following example. Notice that the Time object in my console is already aware of my time zone. I'm not performing any magical transformations here.
>> t = Time.now
=> Wed Jul 15 18:47:33 -0500 2009
>> t.to_s
=> "07/15/2009 18:47"
>> t.to_s(:my_special_format)
=> "18:47 PM"
Calling Time#to_s uses the :default format, or you can pass in the name of the format you'd rather use like I did with :my_special_format.
You can see the various options for formatting a Time object here.

If u don't want to store each user time setting, the only solution is to use javascript time system because it work on user client time. For example i have an application that each time user try it, the app will create some example data with each data have a initial date value "today". At first time, it confuse me a lot because my host server is in australia and lot of user is on western part, so sometime the initial date value is not "today", it said "yesterday" because of different time region.
After a couple day of headache i finally take decision to JUST use javascript time system and include it in the link, so when user click the "try now" link it will also include today date value.
<% javascript_tag do -%>
var today = new Date();
$("trynow").href = "<%= new_invitation_path %>?today=" + today.toLocaleString();
<% end -%>

Add the following to config/environment.rb to handle time correctly and consistently all the time within the context of Rails. It's important to know that it will store your times to the database in UTC -- but this is what you want -- all the conversion is done automatically.
config.time_zone = 'Pacific Time (US & Canada)'
You can run rake time:zones:local from your Rails root directory to get a list of valid time zone strings in your area.

A quick addition to the DATE_FORMAT solution posted above. Your format can be a string, in which case it works as noted above by calling strftime, but you can also define the format as a lambda:
CoreExtensions::Time::Conversions::DATE_FORMATS.merge! :my_complex_format => lambda {|time|
# your code goes here
}

Related

Rails render a time form field in the user's time zone

I am re-writing this post for clarify and to show what I have so far and where exactly I am stuck.
So as you may know, the local_time gem only works when rendering the date and time attributes from the record itself, such as:
local_time(#events.start_time)
But I want to be able to render the datetime or time selects in the user's time zone, yet still save them to the database as UTC. I've been able to achieve this by hard coding it to Easter time by:
- Time.use_zone("Eastern Time (US & Canada)") do
%fieldset.border.border-dark.p-2
%legend Start Time
.input-group
= f.time_select :start_time, {ignore_date: true, minute_step: 15, prompt: true, ampm: true}, {class: "form-select form-select-lg border border-dark ms-2 me-2"}
.text-end.text-muted
= Time.zone
The above code will use Eastern Time (US & Canada) as the time zone the user sees, but at the same time is still saved to the database as UTC.
In order to make this more dynamic, I need to get the client's time zone, which according to this thread, you can obtain the client's IANA time zone by:
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone)
However, I need a way to post this to the events/new controller so that I can use it as a Ruby instance variable to change one line above:
- Time.use_zone(#my_zone) do
In my Events Controller I currently have:
def new
#my_zone = params[:my_zone]
#my_zone = ActiveSupport::TimeZone[TZInfo::Timezone.get('America/Vancouver').period_for_utc(Time.now.utc).utc_offset]
#event = Event.new
The second time that #my_zone is called, we are using an ActiveSupport that would convert the iANA value directly into the TZInfo value that the Time.use_zone uses, as discussed here.
Then once the controller has that value, it can plug it into Time.use_zone() method.
Getting Javascript to post to the controller is where I'm struggling. I attempted to use a similar AJAX call to this thread, such that at the bottom of my new.html.haml:
:javascript
$.post('/events/new', {my_zone: Intl.DateTimeFormat().resolvedOptions().timeZone}))
However, when I try to run that, the browser console says:
Syntax Error: Bare private name can only be used on the left hand side of an 'in' expression.
I can't figure out what's going on with that.
On the other hand, I'm actually on Rails 7 now, which I do know doesn't have UJS by default but rather uses Stimulus, Hotwire and Turbo. So, is there a way I could do the post to the controller via Stimulus, Hotwire and turbo?

Convert Time.now into ActiveSupport::TimeZone

My website has a functionality of using User system time unless they are logged in.
The problem comes when trying to test this functionality, as Time.now.zone gives me a string representation of the zone instead of an ActiveSupport::TimeZone object.
The one I get from Time.now.zone also can't be used to look up the timezone:
Time.now.zone
=> 'BST'
while I need:
=> #<ActiveSupport::TimeZone:0x00007ffe1afd9470 #name="Europe/London", #utc_offset=nil, #tzinfo=#<TZInfo::DataTimezone: Europe/London>>
Use Time.zone instead of Time.now.zone
Thanks to #Stefan, my misunderstanding of Time.now has been cleared, it reporting server machine time, not user's, which of course makes all the sense. Thankfully, when it comes to tests, I can assume the client and server are the on the same machine, so its not a big problem.
I managed to also find a solution to my original problem. The method I was looking for was getlocal, which converts the DateTime object into system time(as mentioned above, that would be the server time, which is ok in tests). That made tests pass on both CircleCI and locally.

How to correctly set time format in strftime method?

I have the following method
<%= message.created_at.strftime("%H:%M") %>
that currently returns 21:00
I need to display a Europe/Kiev time zone which is 3 hours ahead than usual Greenwich's time zone. How do I set it in the method input?
You can use a beautiful gem named "local_time". It takes care of local rendering of the time in your views. It has very nice documentation as well. Visit https://github.com/basecamp/local_time
If you stored time in your database in UTC than that time will always be in UTC when you fetch it. You need to display it according to user's local time zone. You have to do that using JavaScript, check this article.
Or if you need just Kiev time zone and nothing else you can do something like this:
<%= message.created_at.in_time_zone('Europe/Kiev').strftime("%H:%M") %>

TimeWithZone & Time.zone.now integration test fails

In a controller method I set a user's variable activation_sent_at equal to Time.zone.now when an activation email is sent to that user. On the development server this seems to work (although time expressions in my application are 2 hours behind on the local time of my computer).
I want to include an integration test that tests whether activation_sent_at indeed gets set properly. So I included the line:
assert_equal #user.activation_sent_at, Time.zone.now
However, this produces the error:
No visible difference in the ActiveSupport::TimeWithZone#inspect output.
You should look at the implementation of #== on ActiveSupport::TimeWithZone or its members.
I think it's suggesting to use another expression for Time.zone.now in my test. I've looked at different sources, including http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html, but am not sure what to do here. Any suggestions what could be causing this error?
Additional info: Adding puts Time.zone.now and puts #stakeholder.activation_sent_at confirms the two are equal. Not sure what generates the failure/error.
The issue is that the 2 dates are very close to each other but not the same. You can use assert_in_delta
assert_in_delta #user.activation_sent_at, Time.zone.now, 1.second
For RSpec, a similar approach would be to use be_within:
expect(#user.activation_sent_at).to be_within(1.second).of Time.zone.now
The problem is that your times are very close but not quite equal. They are probably off by a few fractions of a second.
One solution to issues like this is a testing gem called timecop. It gives you the ability to mock Time.now so that it will temporarily return a specific value that you can use for comparisons.
The reason is because Time.now or Time.zone.now include milliseconds (when you do a simple put to print the time it doesn't show milliseconds). However, when you persist the timestamp in the database these milliseconds likely get lost unless the db field is configured to store milliseconds. So when you read the value from the db it will not include milliseconds, hence the times are slightly different.
One solution is to remove milliseconds from Time.now. You can do this like so Time.now.change(usec: 0). This should fix the error in the tests.

Rails - How to save datetimes correctly in MongoID and trigger them with whenerver (cronjobs)?

I try to save datetimes into events in MongoDB with Mongoid. The difficulty is that I want to save them for different timezones (e.g. Berlin and London). I want the user (admin) to see the actual time in the timezone so that he does not need to calculate. Afterwards I have a cron job in whenever, which looks every minute for an event to process.
I have the following parameters:
application.rb (I tried without -> standard UTC -> London is ok, Berlin wrong hour)
config.time_zone = 'Berlin' # as the server stays in Germany
mongoid.yml (tried all combinations, need use_activesupport_time_zone to get correct times int oDB though)
use_activesupport_time_zone: true
use_utc: false
schedule.rb (no problem here so far)
every 1.minutes do
runner "Event.activate_due", environment: 'development'
end
event.rb (not sure for which times I am looking now)
def self.activate_due
events_due = Event.where(:eventStart.lte => (Time.now + 1.minute), :eventStart.gte => (Time.now))
events_due.each do |e|
e.activate
end
end
I tried to change the Time.zone in events#new in order to depict the timezone's actual time in the simple_form. It seems to work, but then the controller create method seems to treat the params as standard time zone again and transforms it wrongly (+-1hour when experimenting with London/Berlin). I tried out almost every parameter combination and also Time.now/Time.zone.now.
In event#index I switch through time_zones in order to show the right time.
To complicate things a bit: I save the timezone in the area model, which is connected through areaId with the event model.
How would I show the admin in the form the correct time for the event in the zone he wants to add (not his timezone) and save it correctly in Mongo to trigger it through the cronjob later?
Try the following
def self.activate_due
events_due = Event.where(
:eventStart.to_utc.lte =>(Time.now.to_utc),
:eventStart.to_utc.gte => (1.minute.ago.to_utc))
events_due.each do |e|
e.activate
end
end
Plesse note that this will work with UTC times, so if you have an event in UTC - 7 timezone don't think it will get activated now if the server timezone is UTC + 2

Resources