Save an integer with time_zone_select - ruby-on-rails

I am following the railscast on time zones found here- http://railscasts.com/episodes/106-time-zones-revised
The following is what I am using for the time zone select input. Currently the form saves a string value in the database (ie. "Alaska"). Instead, I'd like to save the UTC offset as an integer. How would I go about doing this?
<%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones

You have two options, you can either use select or you can create a virtual attribute. I would probably prefer the latter option.
Since TimeZone doesn't directly give you an offset in hours (only in seconds or as a string), you can't use collection_select directly, but you can use select like this (utc_offset is in seconds):
f.select :time_zone, ActiveSupport::TimeZone.us_zones.map { |z| [z.name, z.utc_offset / 1.hour] }
If you use a virtual attribute, use the code you are already using but use :time_zone_name instead of :time_zone and then update your model like this:
def time_zone_name=(value)
time_zone = ActiveSupport::TimeZone.new(value).utc_offset / 1.hour
end
def time_zone_name
# time_zone is a number like -9
ActiveSupport::TimeZone.new(time_zone).name
end
I prefer the last option because it enables you to set the time zone by offset or by name even from rails console or from anywhere where you wish to set it.

Related

Rails: active record saving time:type with unwanted format

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.

time_zone_options_for_select to print hours to UTC offsets as value

I have this code to let user select their timezone.
<%= time_zone_options_for_select(#corp.timezone, nil, ActiveSupport::TimeZone) %>
The problem is that code produce this html
<option value="American Samoa">(GMT-11:00) American Samoa</option>
I need it to be like the following
<option value="-11:00">(GMT-11:00) American Samoa</option>
How I can achieve that?
Any particular reason you need to store the offset? I would suggest simplifying like so:
keep view the same
time_zone_options_for_select(#corp.timezone, nil, ActiveSupport::TimeZone)
model
validates :timezone, inclusion: { in: ActiveSupport::TimeZone.zones_map.keys }
convert
DateTime.now.in_time_zone(#corp.timezone)
DateTime.now.in_time_zone("Hawaii")
# => Thu, 07 Aug 2014 23:19:03 HST -10:00
However, to answer your question. See documentation for time_zone_options_for_select along with the source code.
The last argument takes a model. In this case, ActiveSupport::TimeZone. It then uses model.all to get a list of timezones, and extracts label, value (respectively) as [instance.to_s, instance.name].
Note, each TimeZone instance has utc_offset.
TimeZone.new("Hawaii").utc_offset
# => -36000
This means you could pass time_zone_options_for_select your own custom class that implements self.all and patches instance.name.
You could also skip time_zone_options_for_select and use options_for_select, passing in this custom array:
helper
def timezone_options
ActiveSupport::TimeZone.all.map do |zone|
[zone.to_s, timezone_to_offset_string(zone)]
end
end
def timezone_to_offset_string(timezone)
(timezone.utc_offset / 3600).to_s + ":00"
end
view
options_for_select(timezone_options, #corp.timezone)
But then you lose priority option from time_zone_options_for_select. So, as you can see, it's probably best to just stick to conventions.

Date is not saving in correct format controller in RoR

my controller's action
time = Time.now.strftime("%d-%m-%Y")
# render text: time
u=SectionMst.new( :section_name => params[:section_name], :date_added => time , date_updated => time)
u.save
My modal code is
class SectionMst < ActiveRecord::Base
attr_accessible :date_added, :date_updated, :id, :section_name
end
render text:time is giving correct desired format but saving in %Y-%m-%d format
not able to get WHY??
The default db date format is: %Y-%m-%d %H:%M:%S. You can check this by executing Time::DATE_FORMATS[:db] in your rails console.
You can update this format by defining the format of your choice in an initializer file inside config/initializers/. Example:
# config/initializers/date_time_format.rb
Date::DATE_FORMATS[:db] = "%d-%m-%Y"
Time::DATE_FORMATS[:db] = "%d-%m-%Y %H:%M:%S"
This is the problem of your database, actualy DB support different format of dates, when you send date, after formatting it convert into own way so for avoid that use DB function to convert date. as date_format(date, format)
eg. SELECT ProductName, Price, FORMAT(Now(),'YYYY-MM-DD') AS PerDate
FROM Products
for making that sql query take an help from this link
Format used when converting and saving a date string to database with rails

Change default_timezone of active record

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;

Using a duration field in a Rails model

I'm looking for the best way to use a duration field in a Rails model. I would like the format to be HH:MM:SS (ex: 01:30:23). The database in use is sqlite locally and Postgres in production.
I would also like to work with this field so I can take a look at all of the objects in the field and come up with the total time of all objects in that model and end up with something like:
30 records totaling 45 hours, 25 minutes, and 34 seconds.
So what would work best for?
Field type for the migration
Form field for the CRUD forms (hour, minute, second drop downs?)
Least expensive method to generate the total duration of all records in the model
Store as integers in your database (number of seconds, probably).
Your entry form will depend on the exact use case. Dropdowns are painful; better to use small text fields for duration in hours + minutes + seconds.
Simply run a SUM query over the duration column to produce a grand total. If you use integers, this is easy and fast.
Additionally:
Use a helper to display the duration in your views. You can easily convert a duration as integer of seconds to ActiveSupport::Duration by using 123.seconds (replace 123 with the integer from the database). Use inspect on the resulting Duration for nice formatting. (It is not perfect. You may want to write something yourself.)
In your model, you'll probably want attribute readers and writers that return/take ActiveSupport::Duration objects, rather than integers. Simply define duration=(new_duration) and duration, which internally call read_attribute / write_attribute with integer arguments.
In Rails 5, you can use ActiveRecord::Attributes to store ActiveSupport::Durations as ISO8601 strings. The advantage of using ActiveSupport::Duration over integers is that you can use them for date/time calculations right out of the box. You can do things like Time.now + 1.month and it's always correct.
Here's how:
Add config/initializers/duration_type.rb
class DurationType < ActiveRecord::Type::String
def cast(value)
return value if value.blank? || value.is_a?(ActiveSupport::Duration)
ActiveSupport::Duration.parse(value)
end
def serialize(duration)
duration ? duration.iso8601 : nil
end
end
ActiveRecord::Type.register(:duration, DurationType)
Migration
create_table :somethings do |t|
t.string :duration
end
Model
class Something < ApplicationRecord
attribute :duration, :duration
end
Usage
something = Something.new
something.duration = 1.year # 1 year
something.duration = nil
something.duration = "P2M3D" # 2 months, 3 days (ISO8601 string)
Time.now + something.duration # calculation is always correct
I tried using ActiveSupport::Duration but had trouble getting the output to be clear.
You may like ruby-duration, an immutable type that represents some amount of time with accuracy in seconds. It has lots of tests and a Mongoid model field type.
I wanted to also easily parse human duration strings so I went with Chronic Duration. Here's an example of adding it to a model that has a time_spent in seconds field.
class Completion < ActiveRecord::Base
belongs_to :task
belongs_to :user
def time_spent_text
ChronicDuration.output time_spent
end
def time_spent_text= text
self.time_spent = ChronicDuration.parse text
logger.debug "time_spent: '#{self.time_spent_text}' for text '#{text}'"
end
end
I've wrote a some stub to support and use PostgreSQL's interval type as ActiveRecord::Duration.
See this gist (you can use it as initializer in Rails 4.1): https://gist.github.com/Envek/7077bfc36b17233f60ad
Also I've opened pull requests to the Rails there:
https://github.com/rails/rails/pull/16919

Resources