When Rails creates an active record and inserts it, is the created_at value practically the same as Time.now.utc.to_date?
In most cases yes, but it depends on the default timezone configuration option.
ActiveRecord::Timestamp code:
def current_time_from_proper_timezone
default_timezone == :utc ? Time.now.utc : Time.now
end
You can change timezone setting in:
config.active_record.time_zone_aware_attributes = false
If you meant to_date, then no, in the worst case it could be nearly 24 hours off.
If you meant to_datetime, then I believe it will be the same to the second. But note that if you call Time.now immediately before or after creating a record it may not match to the second. I'm curious to know why you need to convert to a DateTime though.
Just test it yourself (let's say your AR class is Post):
dtm_before = Time.now.to_datetime
post = Post.create!(attributes)
dtm_after = Time.now.to_datetime # zone does not matter!
# these differences should be tiny
dtm_before.to_time - post.created_at
dtm_after.to_time - post.created_at
I said the zone doesn't matter because when you're doing time arithmetic, zones are automatically taken into account. Example:
# let's assume your local TZ-offset isn't zero
t = Time.now
t == t.getutc # true
t - t.getutc # 0.0 (these are the exact same instant)
Related
The goal is to compare timestamp to a range of times.
The range of times were defined in the rails schema as t.time for postgresql database. However the data returned upon querying the console attributes a date to the record's field...
start_time: "2000-01-01 08:00:00"
end_time: "2000-01-01 17:59:59"
Now if I want to validate whether a record created_at: "2017-03-18 03:44:04" is in the time range, I am also comparing the date, which is throwing the query into empty-array-land.
What rails or ruby tools can be used in this case in a database-agnostic manner?
def within_time_range?(start_time, end_time, time_check)
t = time_check[11..-1]
t >= start_time[11..-1] && t <= end_time[11..-1]
end
start_time = "2000-01-01 08:00:00"
end_time = "2000-01-01 17:59:59"
within_time_range?(start_time, end_time, "2017-03-18 03:44:04")
#=> false
within_time_range?(start_time, end_time, "2017-03-18 09:05:01")
#=> true
within_time_range?(start_time, end_time, "2017-03-18 19:05:01")
#=> false
Note
start_time[11..-1]
#=> "08:00:00"
I've used String#<= and String#>=, which are obtained from String#<=> and the inclusion of the module Comparable into the String class.
You could force every time object to be parsed at the same date (e.g. 1st of january 2000) :
require 'time'
def parse_time_not_date(string)
time = Time.parse(string)
Time.local(2000, 1, 1, time.hour, time.min, time.sec, time.usec)
end
start_time = parse_time_not_date("2000-01-01 08:00:00")
end_time = parse_time_not_date("2000-01-01 17:59:59")
my_time = parse_time_not_date("2017-03-18 03:44:04")
puts (start_time..end_time).cover?(my_time)
# false
puts (start_time..end_time).cover?(parse_time_not_date("2017-03-18 14:59"))
# true
I am going to chime in, as the discussion from the first answer was pertinent. Cary's answer works for that case. The question was ambivalent in terms of possible solutions: ruby or rails. And hence some potential gum ups. What follows is another way.
With rails, there is an issue in considering time zones. Calling an object of data type time actually stays in UTC. With date_time you are getting a string with + or - hours(or fraction thereof - yay Newfoundland!)
So, processing with rails, the proper way to handle UTC data is to assign it the rails application time zone with in_time_zone, chain to string and then extracting. Thus, comparisons ended up as:
p = Time.parse(#interruptions[0].pause.to_s[11,8])
p >= Time.parse(d_s.start_time.in_time_zone.to_s[11,8]) && p <= Time.parse(d_s.end_time.in_time_zone.to_s[11,8])
Note: could not get [11..-1] working in this context
I need to be able to receive a user-input timestamp, with an optional time zone component, validate that is it a valid ISO 8601 time representation, and parse it according to the user's configured time zone.
I'm using Rails 4.2.6 on Ruby 2.3. I had hoped that Time.zone (ActiveSupport::TimeZone) would have an equivalent implementation to Time::iso8601 so that I could rescue ArgumentError exceptions to determine if the user input was a valid ISO 8601 time representation. Then I could do something like:
user_time_zone = 'America/Los_Angeles' # Would actually be from user's stored settings
params = {when: '2016-04-01T01:01:01'} # Would actually be from user input
# Would actually use Time::use_zone in around_action filter
Time.use_zone(user_time_zone) do
timestamp = Time.zone.iso8601 params[:when]
end
But, alas, no such method exists. And I can't find an equivalent one.
I can't use Time.zone.parse, because it treats ambiguous dates as valid (e.g. Time.zone.parse '04/11/16' # => Tue, 16 Nov 0004 00:00:00 LMT -07:52).
The best alternative I've been able to come up with so far is:
Time.use_zone(user_time_zone) do
old_tz = ENV['TZ']
ENV['TZ'] = Time.zone.name
timestamp = Time.iso8601 params[:when] # => 2016-04-01 01:01:01 -0700
ENV['TZ'] = old_tz
end
But this is ugly, messing around with an environment variable this way doesn't feel proper, and it and certainly isn't Rails-like. How can I validate and parse the time according to the user's time zone in a Rails way?
I suggest that you simply split the assignment into two steps: validate the ISO8601 format first and if valid, parse it:
user_time_zone = 'America/Los_Angeles'
params = { when: '2016-04-01T01:01:01' }
begin
Time.iso8601(params[:when]) # raises ArgumentError if format invalid
rescue ArgumentError => e
puts "invalid time format"
return
end
Time.use_zone(user_time_zone) do
timestamp = Time.zone.parse(params[:when])
end
I think you can still use an around_action for your use case. That's what I use and it works well for me.
In my ApplicationController I have:
class ApplicationController < ActionController::Base
around_action :set_time_zone
def set_time_zone
old_time_zone = Time.zone
Time.zone = user_time_zone
yield
ensure
Time.zone = old_time_zone
end
end
Any calls to Time will use the user's time zone within the scope of the request.
I have a table call periodo with the attribute hour. i pass my time param in this way
hour = Time.parse( splitLine[1] ) #where splitLine[1] is my time but in string
periodo = Periodo.new(:hour => hour.strftime("%H:%M"))
periodo.save
but active record save the records in this way hour: "2000-01-01 07:00:00" , I already set the format in /config/initializers/time_formats.rb
Time::DATE_FORMATS[:default] = "%H:%M"
Date::DATE_FORMATS[:default] = "%H:%M"
and in en.yml too
en:
hello: "Hello world"
time:
formats:
default: "%H:%M"
date:
formats:
default: "%H:%M"
but my records are still saving the year and month :( what i have to do to save just the hour and minutes ???
greetings
Date formats are only valid within your application, not within the database - they are only responsible for the way time objects are displayed to your users and will not affect the way data is stored.
Unfortunately, there is no such a concept like time only in the database (at least I haven't heard about any, and trust me I did search for it as I need it in my current project)
Simplest solution
However, in many cases it makes sense to store only the time of the event. In current project we decided to store it in format of integer 60 * hour + minutes. This is unfortunately where we stopped in the project. :(
One step further
You can then create a helper class (just a scaffold - more work needed like validations casting etc):
class SimpleTime
attr_accessor :hour, :minute
def initialize(hour, minute)
#hour, #minute = hour, minute
end
def self.parse(integer)
return if integer.blank?
new(integer / 60, integer % 60)
end
def to_i
60 * #hour + #minute
end
end
and then override a setter and getter:
class Model < ActiveRecord::Base
def time
#time ||= SimpleTime.parse(super)
end
def time=(value)
super(value.to_i)
end
end
Further fun
Now there are more things you could do. You can for example write extension simple_time to active_record which will automatically redefine setters and getters for a list of passed attributes. You can even wrap it in a small gem and make it real-world-proof (missing validations, string format parsers, handling _before_type_cast values etc).
You have to do nothing. That is activerecord convention on time storing. So if you want to have automatically parsed time in your model from sql database you have to store it in the way AR does. But if you really want to store only hours and minutes, you should change your scheme and use just string instead of datetime in AR migration. So you can store your time like that. and in the model class you can override the attribute getter like:
def some_time
Time.parse(#some_time)
end
Then you can get parsed time object when you call attribute. But that is a bad way actually.
I know there is a method to determine if a certain time is on Daylight Savings Time (Time.now.dst?) but is there a method to give us the next date when Daylight Savings will change?
For example, Google returns Sunday, November 1 as the next Daylight Savings Time change in 2015.
Since these are dates that are based on other values, like the timezone you are working with, it requires a module like ActiveSupport/TZInfo.
require 'active_support/core_ext/time/zones'
tz = TZInfo::Timezone.get('US/Pacific')
# pick a timezone to work with
tz.current_period #returns an object of the current period
=> #<TZInfo::TimezonePeriod: #<TZInfo::TimezoneTransitionDefinition:
#<TZInfo::TimeOrDateTime: 1425808800>,#<TZInfo::TimezoneOffset: -28800,3600,PDT>>,
#<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1446368400>,
#<TZInfo::TimezoneOffset: -28800,0,PST>>>
tz.current_period.local_start.to_s
# => "2015-03-08T03:00:00+00:00"
tz.current_period.local_end.to_s
# => "2015-11-01T02:00:00+00:00"
One thing I haven't figured out is that since regular Ruby Core does this:
Time.now.dst?
# => true
Where is it getting this info? I found the TZInfo classes through ActiveSupport. Is Ruby just getting a boolean value from the OS?
How about this extension of the Time class:
class Time
class << self
def next_dst_change
startdate = Date.today
enddate = Date.today.end_of_year
match = Date.today.to_time.dst? ? false : true
startdate.upto(enddate).find { |date| date.to_time if date.to_time.dst? == match }
end
end
end
Now you can do Time.next_dst_change. You can apply this on your own timezone only but it solves your problem.
In rails, I have the below config for activerecord at first.
config.active_record.default_timezone = :utc
Now, I want to use the local timezone, so I changed it to:
config.active_record.default_timezone = :local
The problem is, I need to shift all the existing data in the date/datetime column to the local timezone.
What is the best way to achieve this?
Why I have to do this is because I have to do aggregation on the local timezone, for example, :group => 'DATE(created_at)', GROUP BY DATE(created_at) will be based on the UTC, but I want to aggregate with one day in local timezone.
I knew how to write a migration file to migrate a certain datetime column. But there are a lot of such column, so I'm seeking for a better solution.
This is dangerous, but here is what I'd do in the migration:
class MigrateDateTimesFromUTCToLocal < ActiveRecord::Migration
def self.up
# Eager load the application, in order to find all the models
# Check your application.rb's load_paths is minimal and doesn't do anything adverse
Rails.application.eager_load!
# Now all the models are loaded. Let's loop through them
# But first, Rails can have multiple models inheriting the same table
# Let's get the unique tables
uniq_models = ActiveRecord::Base.models.uniq_by{ |model| model.table_name }
begin
# Now let's loop
uniq_models.each do |model|
# Since we may be in the middle of many migrations,
# Let's refresh the latest schema for that model
model.reset_column_information
# Filter only the date/time columns
datetime_columns = model.columns.select{ |column| [ :datetime, :date, :time].include? column.type }
# Process them
# Remember *not* to loop through model.all.each, or something like that
# Use plain SQL, since the migrations for many columns in that model may not have run yet
datetime_columns.each do |column|
execute <<-SQL
UPDATE #{model.table_name} SET #{column.name} = /* DB-specific date/time conversion */
SQL
end
rescue
# Probably time to think about your rescue strategy
# If you have tested the code properly in Test/Staging environments
# Then it should run fine in Production
# So if an exception happens, better re-throw it and handle it manually
end
end
end
end
My first advice is to strongly encourage you to not do this. You are opening yourself up to a world of hurt. That said, here is what you want:
class ShootMyFutureSelfInTheFootMigration
def up
Walrus.find_each do |walrus|
married_at_utc = walrus.married_at
walrus.update_column(:married_at, married_at_utc.in_time_zone)
end
end
def down
Walrus.find_each do |walrus|
married_at_local = walrus.married_at
walrus.update_column(:married_at, married_at_local.utc)
end
end
end
You may pass in your preferred timezone into DateTime#in_time_zone, like so:
central_time_zone = ActiveSupport::TimeZone.new("Central Time (US & Canada)")
walrus.update_column(:married_at, married_at_utc.in_time_zone(central_time_zone))
Or you can leave it and Rails will use your current timezone. Note that this isn't where you are, it is where your server is. So if you have users in Iceland and Shanghai, but your server is in California, every single 'local' time zone will be US Pacific Time.
Must you change the data in the database? Can you instead display the dates in local time zone. Does this help: Convert UTC to local time in Rails 3
Like a lot of other people said, you probably don't want to do this.
You can convert the time to a different zone before grouping, all in the database. For example, with postgres, converting to Mountain Standard Time:
SELECT COUNT(id), DATE(created_at AT TIME ZONE 'MST') AS created_at_in_mst
FROM users GROUP BY created_at_in_mst;